diff --git a/.gitignore b/.gitignore index 2f4231d0..c8e897af 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ stats docs .vitest-reports -.auth \ No newline at end of file +.auth +.sanity diff --git a/apps/property-detail-app/.gitignore b/apps/property-detail-app/.gitignore new file mode 100644 index 00000000..aa9909c0 --- /dev/null +++ b/apps/property-detail-app/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +/node_modules +/.pnp +.pnp.js + +# Compiled Sanity Studio +/dist + +# Temporary Sanity runtime, generated by the CLI on every dev server start +/.sanity + +# Logs +/logs +*.log + +# Coverage directory used by testing tools +/coverage + +# Misc +.DS_Store +*.pem + +# Typescript +*.tsbuildinfo + +# Dotenv and similar local-only files +*.local diff --git a/apps/property-detail-app/.lintstagedrc.mjs b/apps/property-detail-app/.lintstagedrc.mjs new file mode 100644 index 00000000..ac300caf --- /dev/null +++ b/apps/property-detail-app/.lintstagedrc.mjs @@ -0,0 +1,4 @@ +// @ts-check +import baseConfig from '../../.lintstagedrc.mjs' + +export default baseConfig diff --git a/apps/property-detail-app/_intents/completeAllTasks.ts b/apps/property-detail-app/_intents/completeAllTasks.ts new file mode 100644 index 00000000..4db888f5 --- /dev/null +++ b/apps/property-detail-app/_intents/completeAllTasks.ts @@ -0,0 +1,16 @@ +// import { defineIntent } from "@sanity/sdk"; + +// some .cjs and esm issues with the old CLI, but we do have this helper +// export default defineIntent({ + +export default { + id: 'completeAllTasks', + action: 'edit', + title: 'Complete All Maintenance Tasks', + filters: [ + { + types: ['property', 'maintenanceSchedule'], + }, + ], + description: 'Bulk complete all tasks for a maintenance schedule without loading the main app', +} diff --git a/apps/property-detail-app/_intents/maintenanceList.ts b/apps/property-detail-app/_intents/maintenanceList.ts new file mode 100644 index 00000000..aa30fdec --- /dev/null +++ b/apps/property-detail-app/_intents/maintenanceList.ts @@ -0,0 +1,16 @@ +// import { defineIntent } from "@sanity/sdk"; + +// some .cjs and esm issues with the old CLI, but we do have this helper +// export default defineIntent({ + +export default { + id: 'maintenanceList', + action: 'edit', + title: 'Maintenance List', + filters: [ + { + types: ['property'], + }, + ], + description: 'View and update the maintenance list for a property', +} diff --git a/apps/property-detail-app/eslint.config.mjs b/apps/property-detail-app/eslint.config.mjs new file mode 100644 index 00000000..161663b1 --- /dev/null +++ b/apps/property-detail-app/eslint.config.mjs @@ -0,0 +1,27 @@ +// @ts-check +import baseESLintConfig from '@repo/config-eslint' +import reactConfig from '@repo/config-eslint/react' + +export default [ + { + ignores: [ + '.DS_Store', + '**/node_modules', + '**/build', + '**/dist', + '.env', + '.env.*', + '!.env.example', + + // Ignore files for PNPM, NPM and YARN + 'pnpm-lock.yaml', + 'package-lock.json', + 'yarn.lock', + + // Ignore files for Sanity TypeGen + 'sanity.types.ts', + ], + }, + ...baseESLintConfig, + ...reactConfig, +] diff --git a/apps/property-detail-app/package.json b/apps/property-detail-app/package.json new file mode 100644 index 00000000..89bdc5a8 --- /dev/null +++ b/apps/property-detail-app/package.json @@ -0,0 +1,39 @@ +{ + "name": "property-detail-app", + "version": "1.0.0", + "private": true, + "keywords": [ + "sanity" + ], + "license": "UNLICENSED", + "type": "module", + "main": "package.json", + "scripts": { + "build": "sanity build", + "dev": "sanity dev", + "lint": "eslint .", + "start": "sanity start", + "tsc": "tsc --noEmit" + }, + "prettier": "@sanity/prettier-config", + "dependencies": { + "@sanity/sdk-react": "workspace:*", + "react": "^19", + "react-dom": "^19", + "react-router": "^7.5.2", + "styled-components": "^6.1.15" + }, + "devDependencies": { + "@repo/config-eslint": "workspace:*", + "@repo/tsconfig": "workspace:*", + "@sanity/prettier-config": "^1.0.3", + "@sanity/sdk": "workspace:*", + "@types/react": "^19", + "@types/react-dom": "^19.1.2", + "eslint": "^9.25.1", + "prettier": "^3.5.3", + "sanity": "^3", + "typescript": "^5.1.6", + "vite": "^6.3.3" + } +} diff --git a/apps/property-detail-app/sanity.cli.ts b/apps/property-detail-app/sanity.cli.ts new file mode 100644 index 00000000..7c1a5157 --- /dev/null +++ b/apps/property-detail-app/sanity.cli.ts @@ -0,0 +1,9 @@ +import {defineCliConfig} from 'sanity/cli' + +export default defineCliConfig({ + app: { + organizationId: 'oF5P8QpKU', + entry: './src/App.tsx', + id: 'uxq5o822rvrg62hyxu16l800', + }, +}) diff --git a/apps/property-detail-app/src/App.css b/apps/property-detail-app/src/App.css new file mode 100644 index 00000000..29a013f0 --- /dev/null +++ b/apps/property-detail-app/src/App.css @@ -0,0 +1,355 @@ +/* Container styling for the app */ +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', + 'Helvetica Neue', sans-serif; +} + +/* Basic reset */ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + background-color: #f9f9f9; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; +} + +h1 { + color: #333; + margin-bottom: 24px; +} + +.property-count { + color: #666; + margin-bottom: 20px; +} + +.property-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.property-link { + text-decoration: none; + color: inherit; +} + +.property-item { + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.2s ease; + background: white; +} + +.property-item:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: #007bff; +} + +.property-info h3 { + margin: 0 0 8px 0; + color: #333; + font-size: 1.2em; +} + +.property-info p { + margin: 4px 0; + color: #666; + font-size: 0.9em; +} + +.broker-name { + font-weight: 500; +} + +.maintenance-type { + color: #007bff; + font-weight: 500; +} + +.maintenance-date { + color: #28a745; +} + +.property-arrow { + font-size: 1.5em; + color: #007bff; +} + +.loading { + text-align: center; + padding: 40px; + color: #666; +} + +.loading-item { + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 20px; + margin-bottom: 16px; + background: #f8f9fa; + color: #666; + text-align: center; +} + +/* Maintenance page specific styles */ +.maintenance-container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; +} + +.maintenance-header { + margin-bottom: 32px; +} + +.back-link { + color: #007bff; + text-decoration: none; + font-size: 0.9em; + margin-bottom: 16px; + display: inline-block; +} + +.back-link:hover { + text-decoration: underline; +} + +.property-details { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 16px; +} + +.detail-item { + background: #f8f9fa; + padding: 8px 12px; + border-radius: 4px; + font-size: 0.9em; +} + +.maintenance-content { + display: flex; + flex-direction: column; + gap: 32px; +} + +.todos-section h2, +.notes-section h2 { + color: #333; + margin-bottom: 16px; +} + +.todos-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.loading-task { + padding: 12px; + background: #f8f9fa; + border-radius: 4px; + color: #666; + text-align: center; +} + +.no-tasks { + padding: 20px; + text-align: center; + color: #666; + background: #f8f9fa; + border-radius: 8px; +} + +.notes-textarea { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-family: inherit; + resize: vertical; +} + +.submit-section { + text-align: center; +} + +.submit-button { + background: #28a745; + color: white; + border: none; + padding: 12px 24px; + border-radius: 4px; + font-size: 1em; + cursor: pointer; + transition: background-color 0.2s; +} + +.submit-button:hover:not(:disabled) { + background: #218838; +} + +.submit-button:disabled { + background: #6c757d; + cursor: not-allowed; +} + +.submit-note { + margin-top: 8px; + color: #666; + font-size: 0.9em; +} + +.error-message, +.no-maintenance { + text-align: center; + padding: 40px; + color: #666; +} + +.success-message { + text-align: center; + padding: 40px; +} + +.checkmark { + font-size: 3em; + color: #28a745; + margin-bottom: 16px; +} + +.success-button { + background: #007bff; + color: white; + text-decoration: none; + padding: 12px 24px; + border-radius: 4px; + display: inline-block; + transition: background-color 0.2s; +} + +.success-button:hover { + background: #0056b3; +} + +/* Enhanced Loading Fallback Styles */ +.loading-header { + margin-bottom: 32px; +} + +.loading-title { + height: 32px; + background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 4px; + margin-bottom: 12px; + width: 300px; +} + +.loading-subtitle { + height: 20px; + background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 4px; + width: 200px; +} + +.loading-spinner-container { + text-align: center; + padding: 40px 0; + margin-bottom: 32px; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid #e0e0e0; + border-top: 3px solid #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 16px; +} + +.loading-text { + color: #666; + font-size: 1.1em; + margin: 0; +} + +.loading-skeleton { + background: #f8f9fa !important; + border-color: #e9ecef !important; + pointer-events: none; +} + +.skeleton-line { + background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 4px; + margin-bottom: 8px; +} + +.skeleton-title { + height: 20px; + width: 70%; +} + +.skeleton-subtitle { + height: 16px; + width: 50%; +} + +.skeleton-details { + height: 14px; + width: 60%; +} + +.skeleton-arrow { + width: 24px; + height: 24px; + background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 50%; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} diff --git a/apps/property-detail-app/src/App.tsx b/apps/property-detail-app/src/App.tsx new file mode 100644 index 00000000..54de17a0 --- /dev/null +++ b/apps/property-detail-app/src/App.tsx @@ -0,0 +1,57 @@ +import './App.css' + +import {type SanityConfig} from '@sanity/sdk' +import {type IntentHandlerPayload, type IntentHandlers, SanityApp} from '@sanity/sdk-react' +import React from 'react' +import {BrowserRouter, useNavigate} from 'react-router' + +import {AppRoutes} from './AppRoutes' +import {CompleteAllTasks} from './components/CompleteAllTasks' +import {LoadingFallback} from './components/LoadingFallback' + +function AppWithRouter(): React.JSX.Element { + const navigate = useNavigate() + + const intentHandlers: IntentHandlers = { + maintenanceList: { + type: 'async', + handler: async (payload: IntentHandlerPayload) => { + if (payload.documentHandle) { + navigate(`/property/${payload.documentHandle.documentId}`) + } + }, + }, + completeAllTasks: { + type: 'component', + handler: CompleteAllTasks, + // Hide the app completely for this bulk operation - shows only our component + hideApp: true, + }, + } + + const sanityConfigs: SanityConfig[] = [ + { + projectId: '9wmez61s', + dataset: 'production', + auth: { + apiHost: 'https://api.sanity.work', + }, + }, + ] + + return ( + }> + + + ) +} + +function App(): React.JSX.Element { + return ( + + + + ) +} + +export default App diff --git a/apps/property-detail-app/src/AppRoutes.tsx b/apps/property-detail-app/src/AppRoutes.tsx new file mode 100644 index 00000000..852e0aa5 --- /dev/null +++ b/apps/property-detail-app/src/AppRoutes.tsx @@ -0,0 +1,14 @@ +import {type JSX} from 'react' +import {Route, Routes} from 'react-router' + +import Maintenance from './Maintenance' +import PropertyList from './PropertyList' + +export function AppRoutes(): JSX.Element { + return ( + + } /> + } /> + + ) +} diff --git a/apps/property-detail-app/src/Maintenance.css b/apps/property-detail-app/src/Maintenance.css new file mode 100644 index 00000000..6e53838f --- /dev/null +++ b/apps/property-detail-app/src/Maintenance.css @@ -0,0 +1,493 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +.maintenance-app { + min-height: 100vh; + background-color: #f9fafb; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; +} + +/* Header */ +.maintenance-header { + background: white; + border-bottom: 1px solid #e5e7eb; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + position: sticky; + top: 0; + z-index: 100; +} + +.header-content { + max-width: 800px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.back-button { + background: none; + border: 1px solid #d1d5db; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + cursor: pointer; + font-size: 0.875rem; + color: #374151; + transition: all 0.2s; +} + +.back-button:hover { + background-color: #f3f4f6; + border-color: #9ca3af; +} + +.header-info h1 { + font-size: 1.5rem; + font-weight: 700; + color: #111827; + margin-bottom: 0.25rem; +} + +.property-info { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.property-address { + font-size: 0.875rem; + color: #374151; + font-weight: 500; +} + +.broker-name { + font-size: 0.75rem; + color: #6b7280; +} + +/* Main Content */ +.maintenance-content { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.maintenance-container { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Cards */ +.info-card, +.checklist-card, +.notes-card { + background: white; + border-radius: 0.5rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + padding: 1.5rem; +} + +.info-card h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; + color: #111827; +} + +.detail-row { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + font-size: 0.875rem; + color: #6b7280; +} + +.icon { + font-size: 1rem; +} + +/* Progress Bar */ +.progress-section { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid #e5e7eb; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + font-size: 0.875rem; + color: #374151; +} + +.progress-percentage { + font-weight: 600; + color: #059669; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #e5e7eb; + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background-color: #059669; + transition: width 0.3s ease; +} + +/* Checklist */ +.checklist-card h3 { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 1rem; + color: #111827; +} + +.todo-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.todo-item { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + padding: 0.75rem; + border: 1px solid #e5e7eb; + border-radius: 0.375rem; + transition: all 0.2s; + gap: 1rem; +} + +.todo-main { + display: flex; + align-items: center; + flex: 1; + gap: 0.75rem; +} + +.todo-item:hover { + border-color: #d1d5db; + background-color: #f9fafb; +} + +.todo-item.completed { + background-color: #f0fdf4; + border-color: #bbf7d0; +} + +.todo-label { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + flex: 1; +} + +.todo-checkbox { + display: none; +} + +.checkmark { + width: 20px; + height: 20px; + border: 2px solid #d1d5db; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + flex-shrink: 0; + margin: 0; +} + +.todo-checkbox:checked + .checkmark { + background-color: #059669; + border-color: #059669; +} + +.todo-checkbox:checked + .checkmark::after { + content: '✓'; + color: white; + font-size: 14px; + font-weight: bold; +} + +.todo-text { + font-size: 0.875rem; + color: #374151; + transition: all 0.2s; + line-height: 1.25rem; + margin: 0; +} + +.todo-item.completed .todo-text { + text-decoration: line-through; + color: #6b7280; +} + +.todo-notes { + margin-left: 2.5rem; + padding-top: 0.25rem; + width: 100%; + flex-basis: 100%; +} + +.todo-notes small { + color: #6b7280; + font-style: italic; +} + +/* Priority Badges */ +.priority-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + flex-shrink: 0; + align-self: flex-start; + white-space: nowrap; +} + +/* Skeleton Loading Styles */ +.skeleton-loading { + opacity: 0.7; + pointer-events: none; +} + +.skeleton-loading .todo-checkbox { + opacity: 0.5; +} + +.skeleton-loading .checkmark { + border-color: #e5e7eb; + background-color: #f3f4f6; +} + +/* Updating state for optimistic updates */ +.todo-item.updating { + opacity: 0.8; + pointer-events: none; +} + +.todo-item.updating .checkmark { + background-color: #f3f4f6; + border-color: #d1d5db; +} + +.skeleton-text { + background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite ease-in-out; + border-radius: 0.25rem; + color: transparent; + min-height: 1.25rem; + display: inline-block; + width: 120px; +} + +@keyframes skeleton-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.priority-high { + background-color: #fee2e2; + color: #991b1b; +} + +.priority-medium { + background-color: #fef3c7; + color: #92400e; +} + +.priority-low { + background-color: #dbeafe; + color: #1e40af; +} + +/* Notes */ +.notes-card h3 { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 1rem; + color: #111827; +} + +.notes-textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + font-size: 0.875rem; + font-family: inherit; + resize: vertical; + min-height: 100px; +} + +.notes-textarea:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Submit Section */ +.submit-section { + display: flex; + justify-content: center; + padding: 1rem 0; +} + +/* Schedule State Dropdown */ +.schedule-state-dropdown { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.dropdown-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.dropdown-label { + font-size: 0.875rem; + font-weight: 500; + color: #374151; +} + +.state-select { + padding: 0.5rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + font-size: 0.875rem; + color: #374151; + background-color: white; + cursor: pointer; + min-width: 150px; +} + +.state-select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.state-action-button { + min-width: 250px; +} + +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-align: center; + text-decoration: none; + display: inline-block; +} + +.btn-primary { + background-color: #3b82f6; + color: white; +} + +.btn-primary:hover:not(:disabled) { + background-color: #2563eb; +} + +.btn-primary:disabled { + background-color: #9ca3af; + cursor: not-allowed; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1rem; +} + +.success-message { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 1rem 1.5rem; + background-color: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 0.375rem; + color: #166534; + font-weight: 500; +} + +/* Error State */ +.error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 50vh; + text-align: center; + gap: 1rem; +} + +.error-state h1 { + font-size: 1.5rem; + color: #374151; +} + +.error-state p { + color: #6b7280; +} + +/* Responsive */ +@media (max-width: 640px) { + .maintenance-content { + padding: 1rem; + } + + .header-content { + padding: 1rem; + } + + .header-info h1 { + font-size: 1.25rem; + } + + .todo-item { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .priority-badge { + align-self: flex-end; + } + + .btn-large { + width: 100%; + } +} diff --git a/apps/property-detail-app/src/Maintenance.tsx b/apps/property-detail-app/src/Maintenance.tsx new file mode 100644 index 00000000..4c3663ae --- /dev/null +++ b/apps/property-detail-app/src/Maintenance.tsx @@ -0,0 +1,183 @@ +import './Maintenance.css' + +import {useDocumentProjection} from '@sanity/sdk-react' +import React, {Suspense, useState} from 'react' +import {Link, useParams} from 'react-router' + +import {MaintenanceTask} from './components/MaintenanceTask' +import {MaintenanceTaskSkeleton} from './components/MaintenanceTaskSkeleton' +import {ScheduleStateDropdown} from './components/ScheduleStateDropdown' + +export default function MaintenanceApp(): React.ReactNode { + const {propertyId} = useParams<{propertyId: string}>() + const [notes, setNotes] = useState('') + const [isSubmitted] = useState(false) + + // Fetch the property document + const {data: property} = useDocumentProjection<{ + address: string + }>({ + documentId: propertyId || '', + documentType: 'property', + projection: '{address}', + }) + + // Fetch the maintenance schedule for this property + const {data} = useDocumentProjection<{ + maintenanceSchedules: Array<{ + _id: string + maintenanceType: string + scheduledDate: string + status: string + notes?: string + assignedBroker: {name: string} + tasks: Array<{_ref: string; _key: string; _type: string}> + }> + }>({ + documentId: propertyId || '', + documentType: 'property', + projection: `{ + maintenanceSchedules[]->{ + _id, + maintenanceType, + scheduledDate, + status, + notes, + assignedBroker->{name}, + tasks[] + } + }`, + }) + + const currentSchedule = data?.maintenanceSchedules?.[0] // Get the latest maintenance schedule + + if (!propertyId) { + return ( +
+
+

Property ID not found

+ + ← Back to Property List + +
+
+ ) + } + + if (!property) { + return ( +
+
Loading property details...
+
+ ) + } + + if (!currentSchedule) { + return ( +
+
+ + ← Back to Property List + +

{property.address}

+
+
+

No maintenance scheduled

+

There are currently no maintenance schedules for this property.

+
+
+ ) + } + + if (isSubmitted) { + return ( +
+
+ + ← Back to Property List + +

Maintenance Completed!

+
+
+
+

Great job!

+

+ The maintenance for {property.address} has been completed. +

+
+ + Back to Properties + +
+
+
+ ) + } + + return ( +
+
+ + ← Back to Property List + +

{property.address}

+
+ + Broker: {currentSchedule.assignedBroker?.name || 'No broker assigned'} + + + Type: {currentSchedule.maintenanceType} + + + Scheduled: {currentSchedule.scheduledDate} + + + Status: {currentSchedule.status} + +
+
+ +
+
+

Maintenance Tasks

+
+ {currentSchedule?.tasks?.map((taskRef) => ( + }> + + + )) || ( +
+

No tasks assigned for this maintenance.

+
+ )} +
+
+ +
+

Additional Notes

+