From 1324f9259fa23be003baa29df6f96dd5b78a1d77 Mon Sep 17 00:00:00 2001 From: Damien Coles Date: Mon, 26 Jan 2026 11:28:04 -0500 Subject: [PATCH] public-ready-init --- .dockerignore | 13 + .env.example | 19 + .gitignore | 26 + .graphqlrc.yaml | 9 + .npmrc | 1 + .prettierignore | 12 + .prettierrc | 16 + CLAUDE.md | 439 ++ Dockerfile | 24 + README.md | 170 + docker-compose.yml | 9 + eslint.config.js | 42 + houdini.config.js | 25 + package-lock.json | 6333 +++++++++++++++++ package.json | 47 + schema.graphql | 4331 +++++++++++ src/app.css | 70 + src/app.d.ts | 13 + src/app.html | 12 + src/lib/assets/favicon.svg | 1 + src/lib/auth.ts | 142 + src/lib/components/entity/AddressCard.svelte | 41 + src/lib/components/entity/ContactCard.svelte | 42 + src/lib/components/entity/EntityCard.svelte | 150 + src/lib/components/entity/EntityTabs.svelte | 103 + .../entity/ExpandableAddressCard.svelte | 264 + src/lib/components/entity/IdSidebar.svelte | 31 + .../components/entity/LoadMoreButton.svelte | 49 + .../components/entity/ProjectScopeCard.svelte | 131 + .../components/entity/ServiceScopeCard.svelte | 98 + src/lib/components/entity/StatusBadge.svelte | 28 + .../components/entity/TeamMemberList.svelte | 75 + src/lib/components/entity/index.ts | 15 + src/lib/components/entity/types.ts | 194 + src/lib/components/layout/Container.svelte | 11 + src/lib/components/layout/Nav.svelte | 422 ++ .../components/session/SessionHeader.svelte | 81 + .../components/session/SessionMediaTab.svelte | 182 + .../session/SessionMediaUploadForm.svelte | 108 + .../components/session/SessionNotesTab.svelte | 269 + .../session/SessionPhotoGrid.svelte | 198 + .../session/SessionSummaryTab.svelte | 278 + src/lib/components/session/SessionTabs.svelte | 125 + .../components/session/SessionTasksTab.svelte | 547 ++ .../session/SessionVideoGrid.svelte | 212 + src/lib/components/session/index.ts | 10 + src/lib/components/session/types.ts | 127 + src/lib/graphql/client.ts | 57 + .../messages/ArchiveConversation.graphql | 6 + .../messages/CreateConversation.graphql | 23 + .../messages/DeleteConversation.graphql | 3 + .../mutations/messages/DeleteMessage.graphql | 3 + .../messages/MarkConversationAsRead.graphql | 6 + .../messages/MuteConversation.graphql | 5 + .../mutations/messages/SendMessage.graphql | 23 + .../notifications/DeleteNotification.graphql | 3 + .../MarkAllNotificationsAsRead.graphql | 3 + .../MarkNotificationAsRead.graphql | 7 + .../profiles/UpdateTeamProfile.graphql | 14 + .../sessions/CloseProjectSession.graphql | 17 + .../sessions/CloseServiceSession.graphql | 25 + .../sessions/OpenProjectSession.graphql | 8 + .../sessions/OpenServiceSession.graphql | 8 + .../sessions/media/RemoveProjectPhoto.graphql | 3 + .../sessions/media/RemoveProjectVideo.graphql | 3 + .../sessions/media/RemoveServicePhoto.graphql | 3 + .../sessions/media/RemoveServiceVideo.graphql | 3 + .../sessions/media/UpdateProjectPhoto.graphql | 30 + .../sessions/media/UpdateProjectVideo.graphql | 30 + .../sessions/media/UpdateServicePhoto.graphql | 30 + .../sessions/media/UpdateServiceVideo.graphql | 30 + .../sessions/notes/CreateProjectNote.graphql | 11 + .../sessions/notes/CreateServiceNote.graphql | 11 + .../sessions/notes/RemoveProjectNote.graphql | 3 + .../sessions/notes/RemoveServiceNote.graphql | 3 + .../sessions/notes/UpdateProjectNote.graphql | 11 + .../sessions/notes/UpdateServiceNote.graphql | 11 + .../sessions/tasks/AddProjectTask.graphql | 26 + .../sessions/tasks/AddServiceTask.graphql | 25 + .../sessions/tasks/RemoveProjectTask.graphql | 26 + .../sessions/tasks/RemoveServiceTask.graphql | 25 + .../accounts/AccountToAddressMap.graphql | 15 + .../queries/accounts/GetAccount.graphql | 91 + .../queries/accounts/GetAccounts.graphql | 24 + .../queries/accounts/GetAddress.graphql | 45 + .../queries/customers/GetCustomer.graphql | 6 + .../queries/customers/GetCustomers.graphql | 6 + .../queries/messages/GetConversation.graphql | 92 + .../messages/GetMyConversations.graphql | 62 + .../messages/GetUnreadMessageCount.graphql | 3 + .../notifications/GetMyNotifications.graphql | 24 + .../notifications/GetNotification.graphql | 24 + .../GetUnreadNotificationCount.graphql | 3 + .../queries/projects/GetProject.graphql | 21 + .../queries/projects/GetProjects.graphql | 45 + .../graphql/queries/reports/GetReport.graphql | 44 + .../queries/reports/GetReports.graphql | 10 + .../queries/scopes/GetProjectScope.graphql | 23 + .../graphql/queries/scopes/GetScope.graphql | 26 + .../queries/services/GetService.graphql | 13 + .../queries/services/GetServices.graphql | 40 + .../sessions/ActiveProjectSession.graphql | 8 + .../sessions/ActiveServiceSession.graphql | 8 + .../sessions/GetProjectSession.graphql | 91 + .../sessions/GetServiceSession.graphql | 90 + .../LatestCompletedProjectSession.graphql | 8 + .../LatestCompletedServiceSession.graphql | 8 + src/lib/graphql/queries/team/GetMe.graphql | 31 + .../queries/team/GetTeamMembers.graphql | 7 + src/lib/index.ts | 1 + src/lib/stores/theme.svelte.ts | 57 + src/lib/utils/date.ts | 21 + src/lib/utils/entity.ts | 311 + src/lib/utils/messages.ts | 321 + src/lib/utils/notifications.ts | 190 + src/lib/utils/relay.ts | 38 + src/lib/utils/session.ts | 585 ++ src/lib/utils/user.ts | 30 + src/routes/+layout.svelte | 68 + src/routes/+layout.ts | 9 + src/routes/+page.svelte | 348 + src/routes/accounts/+layout.ts | 15 + src/routes/accounts/+page.svelte | 73 + src/routes/accounts/+page.ts | 8 + src/routes/accounts/[account]/+page.svelte | 141 + src/routes/accounts/[account]/+page.ts | 8 + src/routes/login/+page.svelte | 50 + src/routes/messages/+page.svelte | 376 + src/routes/messages/+page.ts | 18 + .../messages/[conversation]/+page.svelte | 445 ++ src/routes/messages/[conversation]/+page.ts | 18 + src/routes/messages/new/+page.svelte | 479 ++ src/routes/messages/new/+page.ts | 45 + src/routes/notifications/+page.svelte | 312 + src/routes/notifications/+page.ts | 18 + .../notifications/[notification]/+page.svelte | 238 + .../notifications/[notification]/+page.ts | 16 + src/routes/profile/+layout.ts | 15 + src/routes/profile/+page.svelte | 521 ++ src/routes/profile/+page.ts | 8 + src/routes/projects/+layout.ts | 15 + src/routes/projects/+page.svelte | 535 ++ src/routes/projects/+page.ts | 132 + src/routes/projects/[project]/+page.svelte | 380 + src/routes/projects/[project]/+page.ts | 70 + src/routes/reports/+layout.ts | 15 + src/routes/reports/+page.svelte | 140 + src/routes/reports/+page.ts | 27 + src/routes/reports/[report]/+page.svelte | 554 ++ src/routes/reports/[report]/+page.ts | 15 + src/routes/services/+layout.ts | 15 + src/routes/services/+page.svelte | 538 ++ src/routes/services/+page.ts | 130 + src/routes/services/[service]/+page.svelte | 384 + src/routes/services/[service]/+page.ts | 58 + .../sessions/projects/[session]/+page.svelte | 492 ++ .../sessions/projects/[session]/+page.ts | 63 + .../sessions/services/[session]/+page.svelte | 477 ++ .../sessions/services/[session]/+page.ts | 63 + static/robots.txt | 3 + svelte.config.js | 18 + tsconfig.json | 15 + vite.config.ts | 13 + 163 files changed, 25843 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .graphqlrc.yaml create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 eslint.config.js create mode 100644 houdini.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 schema.graphql create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/lib/assets/favicon.svg create mode 100644 src/lib/auth.ts create mode 100644 src/lib/components/entity/AddressCard.svelte create mode 100644 src/lib/components/entity/ContactCard.svelte create mode 100644 src/lib/components/entity/EntityCard.svelte create mode 100644 src/lib/components/entity/EntityTabs.svelte create mode 100644 src/lib/components/entity/ExpandableAddressCard.svelte create mode 100644 src/lib/components/entity/IdSidebar.svelte create mode 100644 src/lib/components/entity/LoadMoreButton.svelte create mode 100644 src/lib/components/entity/ProjectScopeCard.svelte create mode 100644 src/lib/components/entity/ServiceScopeCard.svelte create mode 100644 src/lib/components/entity/StatusBadge.svelte create mode 100644 src/lib/components/entity/TeamMemberList.svelte create mode 100644 src/lib/components/entity/index.ts create mode 100644 src/lib/components/entity/types.ts create mode 100644 src/lib/components/layout/Container.svelte create mode 100644 src/lib/components/layout/Nav.svelte create mode 100644 src/lib/components/session/SessionHeader.svelte create mode 100644 src/lib/components/session/SessionMediaTab.svelte create mode 100644 src/lib/components/session/SessionMediaUploadForm.svelte create mode 100644 src/lib/components/session/SessionNotesTab.svelte create mode 100644 src/lib/components/session/SessionPhotoGrid.svelte create mode 100644 src/lib/components/session/SessionSummaryTab.svelte create mode 100644 src/lib/components/session/SessionTabs.svelte create mode 100644 src/lib/components/session/SessionTasksTab.svelte create mode 100644 src/lib/components/session/SessionVideoGrid.svelte create mode 100644 src/lib/components/session/index.ts create mode 100644 src/lib/components/session/types.ts create mode 100644 src/lib/graphql/client.ts create mode 100644 src/lib/graphql/mutations/messages/ArchiveConversation.graphql create mode 100644 src/lib/graphql/mutations/messages/CreateConversation.graphql create mode 100644 src/lib/graphql/mutations/messages/DeleteConversation.graphql create mode 100644 src/lib/graphql/mutations/messages/DeleteMessage.graphql create mode 100644 src/lib/graphql/mutations/messages/MarkConversationAsRead.graphql create mode 100644 src/lib/graphql/mutations/messages/MuteConversation.graphql create mode 100644 src/lib/graphql/mutations/messages/SendMessage.graphql create mode 100644 src/lib/graphql/mutations/notifications/DeleteNotification.graphql create mode 100644 src/lib/graphql/mutations/notifications/MarkAllNotificationsAsRead.graphql create mode 100644 src/lib/graphql/mutations/notifications/MarkNotificationAsRead.graphql create mode 100644 src/lib/graphql/mutations/profiles/UpdateTeamProfile.graphql create mode 100644 src/lib/graphql/mutations/sessions/CloseProjectSession.graphql create mode 100644 src/lib/graphql/mutations/sessions/CloseServiceSession.graphql create mode 100644 src/lib/graphql/mutations/sessions/OpenProjectSession.graphql create mode 100644 src/lib/graphql/mutations/sessions/OpenServiceSession.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/RemoveProjectPhoto.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/RemoveProjectVideo.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/RemoveServicePhoto.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/RemoveServiceVideo.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/UpdateProjectPhoto.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/UpdateProjectVideo.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/UpdateServicePhoto.graphql create mode 100644 src/lib/graphql/mutations/sessions/media/UpdateServiceVideo.graphql create mode 100644 src/lib/graphql/mutations/sessions/notes/CreateProjectNote.graphql create mode 100644 src/lib/graphql/mutations/sessions/notes/CreateServiceNote.graphql create mode 100644 src/lib/graphql/mutations/sessions/notes/RemoveProjectNote.graphql create mode 100644 src/lib/graphql/mutations/sessions/notes/RemoveServiceNote.graphql create mode 100644 src/lib/graphql/mutations/sessions/notes/UpdateProjectNote.graphql create mode 100644 src/lib/graphql/mutations/sessions/notes/UpdateServiceNote.graphql create mode 100644 src/lib/graphql/mutations/sessions/tasks/AddProjectTask.graphql create mode 100644 src/lib/graphql/mutations/sessions/tasks/AddServiceTask.graphql create mode 100644 src/lib/graphql/mutations/sessions/tasks/RemoveProjectTask.graphql create mode 100644 src/lib/graphql/mutations/sessions/tasks/RemoveServiceTask.graphql create mode 100644 src/lib/graphql/queries/accounts/AccountToAddressMap.graphql create mode 100644 src/lib/graphql/queries/accounts/GetAccount.graphql create mode 100644 src/lib/graphql/queries/accounts/GetAccounts.graphql create mode 100644 src/lib/graphql/queries/accounts/GetAddress.graphql create mode 100644 src/lib/graphql/queries/customers/GetCustomer.graphql create mode 100644 src/lib/graphql/queries/customers/GetCustomers.graphql create mode 100644 src/lib/graphql/queries/messages/GetConversation.graphql create mode 100644 src/lib/graphql/queries/messages/GetMyConversations.graphql create mode 100644 src/lib/graphql/queries/messages/GetUnreadMessageCount.graphql create mode 100644 src/lib/graphql/queries/notifications/GetMyNotifications.graphql create mode 100644 src/lib/graphql/queries/notifications/GetNotification.graphql create mode 100644 src/lib/graphql/queries/notifications/GetUnreadNotificationCount.graphql create mode 100644 src/lib/graphql/queries/projects/GetProject.graphql create mode 100644 src/lib/graphql/queries/projects/GetProjects.graphql create mode 100644 src/lib/graphql/queries/reports/GetReport.graphql create mode 100644 src/lib/graphql/queries/reports/GetReports.graphql create mode 100644 src/lib/graphql/queries/scopes/GetProjectScope.graphql create mode 100644 src/lib/graphql/queries/scopes/GetScope.graphql create mode 100644 src/lib/graphql/queries/services/GetService.graphql create mode 100644 src/lib/graphql/queries/services/GetServices.graphql create mode 100644 src/lib/graphql/queries/sessions/ActiveProjectSession.graphql create mode 100644 src/lib/graphql/queries/sessions/ActiveServiceSession.graphql create mode 100644 src/lib/graphql/queries/sessions/GetProjectSession.graphql create mode 100644 src/lib/graphql/queries/sessions/GetServiceSession.graphql create mode 100644 src/lib/graphql/queries/sessions/LatestCompletedProjectSession.graphql create mode 100644 src/lib/graphql/queries/sessions/LatestCompletedServiceSession.graphql create mode 100644 src/lib/graphql/queries/team/GetMe.graphql create mode 100644 src/lib/graphql/queries/team/GetTeamMembers.graphql create mode 100644 src/lib/index.ts create mode 100644 src/lib/stores/theme.svelte.ts create mode 100644 src/lib/utils/date.ts create mode 100644 src/lib/utils/entity.ts create mode 100644 src/lib/utils/messages.ts create mode 100644 src/lib/utils/notifications.ts create mode 100644 src/lib/utils/relay.ts create mode 100644 src/lib/utils/session.ts create mode 100644 src/lib/utils/user.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+layout.ts create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/accounts/+layout.ts create mode 100644 src/routes/accounts/+page.svelte create mode 100644 src/routes/accounts/+page.ts create mode 100644 src/routes/accounts/[account]/+page.svelte create mode 100644 src/routes/accounts/[account]/+page.ts create mode 100644 src/routes/login/+page.svelte create mode 100644 src/routes/messages/+page.svelte create mode 100644 src/routes/messages/+page.ts create mode 100644 src/routes/messages/[conversation]/+page.svelte create mode 100644 src/routes/messages/[conversation]/+page.ts create mode 100644 src/routes/messages/new/+page.svelte create mode 100644 src/routes/messages/new/+page.ts create mode 100644 src/routes/notifications/+page.svelte create mode 100644 src/routes/notifications/+page.ts create mode 100644 src/routes/notifications/[notification]/+page.svelte create mode 100644 src/routes/notifications/[notification]/+page.ts create mode 100644 src/routes/profile/+layout.ts create mode 100644 src/routes/profile/+page.svelte create mode 100644 src/routes/profile/+page.ts create mode 100644 src/routes/projects/+layout.ts create mode 100644 src/routes/projects/+page.svelte create mode 100644 src/routes/projects/+page.ts create mode 100644 src/routes/projects/[project]/+page.svelte create mode 100644 src/routes/projects/[project]/+page.ts create mode 100644 src/routes/reports/+layout.ts create mode 100644 src/routes/reports/+page.svelte create mode 100644 src/routes/reports/+page.ts create mode 100644 src/routes/reports/[report]/+page.svelte create mode 100644 src/routes/reports/[report]/+page.ts create mode 100644 src/routes/services/+layout.ts create mode 100644 src/routes/services/+page.svelte create mode 100644 src/routes/services/+page.ts create mode 100644 src/routes/services/[service]/+page.svelte create mode 100644 src/routes/services/[service]/+page.ts create mode 100644 src/routes/sessions/projects/[session]/+page.svelte create mode 100644 src/routes/sessions/projects/[session]/+page.ts create mode 100644 src/routes/sessions/services/[session]/+page.svelte create mode 100644 src/routes/sessions/services/[session]/+page.ts create mode 100644 static/robots.txt create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e4a2a21 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +.git +.gitignore +.houdini +.svelte-kit +build +dist +*.log +*.md +.env* +!.env.example +.vscode +.idea diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..22c33ad --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# GraphQL API +PUBLIC_GRAPHQL_URL=http://localhost:8000/graphql/ + +# Ory Kratos (Authentication) +PUBLIC_KRATOS_URL=http://localhost:4433 + +# Calendar Service +PUBLIC_CALENDAR_API_URL=http://localhost:8001 +PUBLIC_CALENDAR_API_KEY=your-calendar-api-key + +# Email Service +PUBLIC_EMAIL_API_URL=http://localhost:8002 +PUBLIC_EMAIL_API_KEY=your-email-api-key + +# Houdini Schema Introspection (development only) +USER_ID=your-dev-user-id +USER_PROFILE_TYPE=TeamProfileType +OATHKEEPER_SECRET=your-oathkeeper-secret +DJANGO_PROFILE_ID=your-dev-profile-id diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b99eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +.houdini +.idea \ No newline at end of file diff --git a/.graphqlrc.yaml b/.graphqlrc.yaml new file mode 100644 index 0000000..5eb640e --- /dev/null +++ b/.graphqlrc.yaml @@ -0,0 +1,9 @@ +projects: + default: + schema: + - ./schema.graphql + - ./.houdini/graphql/schema.graphql + documents: + - '**/*.gql' + - '**/*.svelte' + - ./.houdini/graphql/documents.gql diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..408263d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ + +# Generated files +schema.graphql diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8103a0b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/app.css" +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4b477e1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,439 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a **SvelteKit 5** frontend application for Nexus, built with: + +- **SvelteKit** as the meta-framework +- **Houdini** for GraphQL client operations +- **Tailwind CSS v4** for styling +- **TypeScript** with strict mode enabled +- **Node adapter** for production deployment + +### Relationship to nexus-5-frontend-1 + +This project (nexus-5-frontend-2) is a **streamlined team-focused application** that abstracts functionality from nexus-5-frontend-1 (the admin interface). + +**Key Differences**: + +- **nexus-5-frontend-1**: Admin-focused dashboard with comprehensive controls +- **nexus-5-frontend-2**: Team-focused app with simplified workflows for field/service operations + +**Using nexus-5-frontend-1 as Reference**: + +- ✅ **DO** reference nexus-5-frontend-1 for: + - Understanding business logic and data flow + - GraphQL query structures and data models + - UI component patterns and layouts + - Understanding feature requirements and scope + +- ❌ **DO NOT** replicate from nexus-5-frontend-1: + - Dashboard patterns (this app has no dashboard) + - Admin-specific features + - Complex data loading patterns (this app uses simpler SvelteKit patterns) + - Overly complex UI components + +**When to Reference**: + +1. When understanding what data a feature needs (check the GraphQL queries) +2. When understanding business logic (how tasks, sessions, scopes work) +3. When you need UI inspiration for similar features +4. When clarifying requirements for features that exist in both apps + +**Important**: Always ask the user if you're unclear whether a pattern from nexus-5-frontend-1 should be replicated or simplified for this team-focused app. + +## Development Commands + +```bash +# Start development server (runs on https://local.example.com) +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview + +# Type checking +npm run check + +# Type checking in watch mode +npm run check:watch + +# Format code +npm run format + +# Lint code (runs prettier check + eslint) +npm run lint +``` + +## Docker Deployment + +The application is containerized with Docker for production deployment. + +### Docker Files + +- **`Dockerfile`**: Multi-stage build (Node 22 Alpine) + - Stage 1: Install dependencies and build the SvelteKit app + - Stage 2: Production image with only built output and production dependencies +- **`docker-compose.yml`**: Simple deployment configuration +- **`.dockerignore`**: Excludes node_modules, .houdini, build artifacts, etc. + +### Running with Docker + +```bash +# Build and run with docker-compose +docker-compose up --build + +# Run in detached mode +docker-compose up -d --build + +# Stop the container +docker-compose down +``` + +### Configuration + +The container exposes port 3000 internally, mapped to port 7000 on the host by default. Environment variables: + +- `NODE_ENV=production` +- `PORT=3000` +- `HOST=0.0.0.0` + +To change the external port, modify `docker-compose.yml`: + +```yaml +ports: + - 'YOUR_PORT:3000' +``` + +## Architecture + +### GraphQL Integration (Houdini) + +This project uses **Houdini** for type-safe GraphQL operations: + +- **Client Configuration**: `src/client.ts` - Houdini client connects to GraphQL endpoint at `http://192.168.100.174:5500/graphql/` +- **Schema**: `schema.graphql` at project root +- **Generated Code**: `.houdini/` directory (gitignored, auto-generated) +- **GraphQL Documents**: Write queries/mutations in `.gql` files or inline in `.svelte` components +- **Config**: `houdini.config.js` - Includes authentication headers for schema watching: + - `X-USER-ID` + - `X-USER-PROFILE-TYPE` + - `X-OATHKEEPER-SECRET` + - `X-DJANGO-PROFILE-ID` + +Authentication headers are read from environment variables in `.env` file. + +### Path Aliases + +- `$houdini` → `.houdini/` - Access Houdini generated types and runtime +- Standard SvelteKit aliases (`$lib`, `$app`, etc.) are available + +### Development Server + +The dev server is configured to run on `local.example.com` with HTTPS enabled via `vite-plugin-mkcert`. + +### TypeScript Configuration + +- **Strict mode** enabled +- **Root dirs** include `.houdini/types` for Houdini type integration +- Module resolution set to `bundler` + +### Vite Plugins + +Configured in `vite.config.ts`: + +1. `houdini` - GraphQL client integration +2. `tailwindcss` - Tailwind CSS v4 +3. `sveltekit` - SvelteKit integration +4. `devtoolsJson` - Development tools +5. `mkcert` - Local HTTPS certificates + +### Environment Variables + +The `.env` file contains: + +- API endpoints and keys for Calendar and Email services +- Authentication credentials for GraphQL schema introspection +- All Houdini auth headers (USER_ID, USER_PROFILE_TYPE, OATHKEEPER_SECRET, DJANGO_PROFILE_ID) + +**Important**: These credentials are for development only. + +### ESLint Configuration + +Uses the new flat config format (`eslint.config.js`) with: + +- TypeScript ESLint recommended rules +- Svelte plugin with recommended rules +- Prettier integration +- `no-undef` disabled for TypeScript files (as recommended by typescript-eslint) + +## Working with GraphQL + +### Schema Updates + +When the GraphQL schema changes: + +1. Update `schema.graphql` manually, or +2. Houdini's `watchSchema` will auto-update during development +3. Run `npm run prepare` or restart dev server to regenerate types + +### Writing Queries + +Queries can be written: + +- In separate `.gql` files +- Inline in `.svelte` components using Houdini's Svelte integration + +GraphQL documents are automatically discovered in: + +- `**/*.gql` files +- `**/*.svelte` components +- `.houdini/graphql/documents.gql` + +## Project Structure + +``` +src/ +├── app.d.ts # SvelteKit app types +├── app.css # Global styles (Tailwind imports) +├── app.html # HTML template +├── client.ts # Houdini client setup +├── lib/ +│ ├── assets/ # Static assets (favicon, etc.) +│ ├── components/ # Reusable Svelte components +│ │ ├── entity/ # Entity-related components (StatusBadge, TeamMemberList, etc.) +│ │ ├── layout/ # Layout components (Container, Navigation, etc.) +│ │ └── ui/ # Generic UI components +│ ├── graphql/ # GraphQL documents +│ │ ├── queries/ # Query .gql files organized by entity +│ │ └── mutations/ # Mutation .gql files +│ └── utils/ # Utility functions (date formatting, relay helpers, etc.) +└── routes/ # SvelteKit file-based routing + ├── +layout.svelte # Root layout with navigation + ├── +page.svelte # Home page + ├── services/ # Services list and detail pages + ├── projects/ # Projects list and detail pages + ├── reports/ # Reports list and detail pages + ├── messages/ # Messaging pages + └── sessions/ # Session pages (services/projects) +``` + +### Key Directories + +- **`src/lib/components/entity/`**: Shared components for displaying entity data (services, projects). Includes `StatusBadge`, `TeamMemberList`, `AddressCard`, `IdSidebar`, scope cards, etc. +- **`src/lib/graphql/`**: GraphQL documents organized by entity type. Queries and mutations are in separate `.gql` files. +- **`src/lib/utils/`**: Helper functions like `formatDate()`, `fromGlobalId()` (Relay ID conversion), etc. + +## Important Notes + +- This project uses **Svelte 5** - use modern Svelte 5 syntax (runes, snippets, etc.) +- **Tailwind CSS v4** is configured - use the latest v4 features and syntax +- The project uses **adapter-node** for production builds +- HTTPS is enabled in development - the server runs on `local.example.com` + +## Coding Standards + +### Svelte 5 Specific Rules + +#### SVG `fill` Attribute + +**IMPORTANT**: Never use the `fill` attribute directly on SVG elements in Svelte 5. Always use the `style` attribute instead. + +❌ **Incorrect**: + +```svelte + + +``` + +✅ **Correct**: + +```svelte + + +``` + +**Reason**: The `fill` attribute is considered obsolete in Svelte 5 and will trigger warnings. Use inline styles instead. + +#### HTML Semantics - Button Content + +**IMPORTANT**: Buttons can only contain phrasing content (inline elements). Never place block-level elements like `
` inside ` +``` + +✅ **Correct**: + +```svelte + +``` + +**Valid phrasing content elements** (safe to use in buttons): + +- ``, ``, ``, ``, ``, ``, `` +- ``, ``, ``, ``, ``, ``, `` + +**Invalid block elements** (never use in buttons): + +- `
`, `

`, ``, `

    `, `
      `, `
      `, `
      ` + +**Reason**: HTML5 semantic rules require button elements to only contain phrasing content. Block elements will trigger validation warnings and may cause accessibility issues. + +### TypeScript Standards + +#### Use Houdini Generated Types + +**IMPORTANT**: Never use `any` or `unknown` when Houdini provides generated types. + +❌ **Incorrect**: + +```typescript +let scopeData = $state<{ scope?: any } | null>(null); +tasks.filter((t: any) => ...) +``` + +✅ **Correct**: + +```typescript +import type { GetScope$result } from '$houdini'; +let scopeData = $state(null); +tasks.filter((t) => ...) +``` + +**Available Houdini types**: + +- Import result types: `GetQueryName$result` +- Import input types: `GetQueryName$input` +- Types are automatically generated in `.houdini/artifacts/` + +**Reason**: Houdini generates complete TypeScript types from GraphQL schema. Using these types provides full type safety and autocomplete. + +## UI Patterns + +### Responsive Design + +The app targets mobile devices (360px minimum width) as well as desktop. Common patterns: + +**Mobile Cards / Desktop Table Pattern:** + +For data-heavy pages like reports, use separate layouts: + +```svelte + +
      + {#each items as item} + + + + {/each} +
      + + + +``` + +**Clickable Table Rows:** + +Make entire rows clickable by wrapping each cell's content in anchor tags: + +```svelte + + + + {name} + + + + + {otherValue} + + + +``` + +## Data Patterns + +### Pagination Pattern (Services & Projects) + +The services (`/services`) and projects (`/projects`) pages use a hybrid server/client pagination pattern: + +**How it works:** + +1. **Server Load** (`+page.ts`): + - Fetches first 20 items per tab (scheduled, in_progress, completed) + - Applies month/year filter from URL params (`?month=X&year=Y`) + - Passes `teamProfileId`, `dateFilter`, and `pageSize` to client for subsequent fetches + +2. **Client State** (`+page.svelte`): + - Uses `$state` to accumulate items (initialized from server data via `$effect`) + - Tracks cursor and `hasMore` state per tab + - "Load More" button triggers client-side fetch using Houdini store classes + +3. **Tab-specific behavior:** + - **Scheduled**: `SCHEDULED` + `CANCELLED` statuses, ascending date order + - **In Progress**: `IN_PROGRESS` status only, no pagination (typically few items) + - **Completed**: `COMPLETED` status only, descending date order + +**Client-side load more pattern:** + +```typescript +import { GetServicesByTeamMemberStore } from '$houdini'; +import { ServiceChoices, DateOrdering } from '$houdini'; + +async function loadMoreScheduled() { + if (!scheduledCursor || loadingMoreScheduled) return; + loadingMoreScheduled = true; + + try { + const store = new GetServicesByTeamMemberStore(); + const result = await store.fetch({ + variables: { + teamProfileId: data.teamProfileId, + first: data.pageSize, + after: scheduledCursor, + filters: { + date: data.dateFilter, + status: { inList: [ServiceChoices.SCHEDULED, ServiceChoices.CANCELLED] } + }, + ordering: DateOrdering.ASC + } + }); + + if (result.data?.getServicesByTeamMember) { + const connection = result.data.getServicesByTeamMember; + scheduledItems = [...scheduledItems, ...connection.edges.map((e) => e.node)]; + scheduledCursor = connection.pageInfo.endCursor; + scheduledHasMore = connection.pageInfo.hasNextPage; + } + } finally { + loadingMoreScheduled = false; + } +} +``` + +**Key points:** + +- Month/year filter determines the data set; pagination loads more within that set +- Tab counts show "20+" when more pages exist +- No URL navigation on "Load More" - results append in place diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..457bca2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM node:22-alpine + +WORKDIR /app + +COPY --from=builder /app/build ./build +COPY --from=builder /app/package*.json ./ +RUN npm ci --omit=dev + +ENV NODE_ENV=production +ENV PORT=3000 + +EXPOSE 3000 + +CMD ["node", "build"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b2140f --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Nexus 5 Frontend 2 - Team App + +Streamlined team-focused mobile application for the Nexus 5 platform, designed for field operations and simplified workflows. + +## Overview + +This is the second iteration of the Nexus 5 frontend, built as a lightweight team-focused application that abstracts functionality from the admin dashboard (frontend-1). It's optimized for mobile devices and field workers who need quick access to their assigned work. + +## Tech Stack + +- **SvelteKit 5** - Latest SvelteKit with Svelte 5 runes +- **Houdini** - Type-safe GraphQL client +- **Tailwind CSS v4** - Next-generation Tailwind +- **TypeScript** - Strict mode enabled +- **Node adapter** - Production deployment + +## Evolution + +| Feature | nexus-5-frontend-1 | nexus-5-frontend-2 | +|---------|---------------------|---------------------| +| **Focus** | Admin dashboard | Team mobile app | +| **Complexity** | Comprehensive | Streamlined | +| **Target Users** | Admins, Team Leaders | Field technicians | +| **Svelte Version** | Svelte 4 | Svelte 5 (runes) | +| **Tailwind** | v3 | v4 | +| **UI Library** | Flowbite | Custom components | + +## Features + +- **Service List** - View and manage assigned services +- **Project List** - Track assigned projects +- **Work Sessions** - Open, manage, and close work sessions +- **Task Completion** - Mark tasks complete during sessions +- **Photo/Video Upload** - Document work with media +- **Session Notes** - Add notes during work sessions +- **Reports** - View and access reports +- **Messages** - Team communication +- **Notifications** - Real-time notifications +- **Profile** - Personal profile management + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- Access to Nexus 5 GraphQL API + +### Development + +```bash +# Install dependencies +npm install + +# Start development server +npm run dev + +# Build for production +npm run build + +# Type checking +npm run check +``` + +### Docker Deployment + +```bash +# Build and run with docker-compose +docker-compose up --build + +# Run in detached mode +docker-compose up -d --build +``` + +### Environment Variables + +See `.env.example` for required configuration. + +## Project Structure + +``` +src/ +├── lib/ +│ ├── components/ +│ │ ├── entity/ # Entity display components +│ │ ├── layout/ # Layout components +│ │ └── session/ # Session management +│ ├── graphql/ +│ │ ├── mutations/ # GraphQL mutations +│ │ └── queries/ # GraphQL queries +│ ├── stores/ # Svelte stores +│ └── utils/ # Utility functions +└── routes/ + ├── accounts/ # Account viewing + ├── messages/ # Team messages + ├── notifications/ # Notifications + ├── profile/ # User profile + ├── projects/ # Project list + ├── reports/ # Reports + ├── services/ # Service list + └── sessions/ # Work sessions +``` + +## Key Patterns + +### Svelte 5 Runes + +Uses modern Svelte 5 syntax with runes: + +```svelte + +``` + +### Mobile-First Design + +Optimized for mobile devices with responsive layouts: + +```svelte + +
      + {#each items as item} + + {/each} +
      + + + +
+ {:else} +
+

No labor breakdown available

+
+ {/if} +
+ {:else} +
Report not found
+ {/if} + + diff --git a/src/routes/reports/[report]/+page.ts b/src/routes/reports/[report]/+page.ts new file mode 100644 index 0000000..d785321 --- /dev/null +++ b/src/routes/reports/[report]/+page.ts @@ -0,0 +1,15 @@ +import { load_GetReport, load_AccountToAddressMap, load_GetCustomers } from '$houdini'; + +export const load = async (event) => { + const [report, accountToAddressMap, customers] = await Promise.all([ + load_GetReport({ event, variables: { id: event.params.report } }), + load_AccountToAddressMap({ event }), + load_GetCustomers({ event }) + ]); + + return { + ...report, + accountToAddressMap, + customers + }; +}; diff --git a/src/routes/services/+layout.ts b/src/routes/services/+layout.ts new file mode 100644 index 0000000..73fed09 --- /dev/null +++ b/src/routes/services/+layout.ts @@ -0,0 +1,15 @@ +import type { LayoutLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; +import { checkSession } from '$lib/auth'; + +export const load: LayoutLoad = async ({ url, fetch }) => { + // Check authentication using SvelteKit's fetch + const session = await checkSession(fetch); + if (!session || !session.active) { + // Preserve the original URL so we can redirect back after login + const returnTo = encodeURIComponent(url.pathname + url.search); + throw redirect(307, `/login?return_to=${returnTo}`); + } + + return { session }; +}; diff --git a/src/routes/services/+page.svelte b/src/routes/services/+page.svelte new file mode 100644 index 0000000..8292972 --- /dev/null +++ b/src/routes/services/+page.svelte @@ -0,0 +1,538 @@ + + + +
+
+

+ Services +

+ + +
+
+ + + + +
+ +
+ + + + +
+ + {#if page.url.searchParams.has('month') || page.url.searchParams.has('year')} + + {/if} +
+
+ + {#if loading} +
+
+
+

Loading services...

+
+
+ {:else if navigatingToSession} +
+
+
+

Opening session...

+
+
+ {:else if error} +
+
+ + + +
+

Error Loading Services

+

{error?.[0]?.message}

+
+
+
+ {:else if hasAnyServices} + + + +
+ + {#if activeTab === 'scheduled'} + {#if scheduledItems.length > 0} +
+ {#each scheduledItems as service (service.id)} + {@const addressInfo = service.accountAddressId + ? accountAddressMap[service.accountAddressId] + : null} + handleStartSession(service) + }} + /> + {/each} +
+ + {:else} +
+

No scheduled services found.

+
+ {/if} + {/if} + + + {#if activeTab === 'in_progress'} + {#if inProgressItems.length > 0} +
+ {#each inProgressItems as service (service.id)} + {@const addressInfo = service.accountAddressId + ? accountAddressMap[service.accountAddressId] + : null} + handleOpenSession(service) + }} + /> + {/each} +
+ {:else} +
+

No services in progress.

+
+ {/if} + {/if} + + + {#if activeTab === 'completed'} + {#if completedItems.length > 0} +
+ {#each completedItems as service (service.id)} + {@const addressInfo = service.accountAddressId + ? accountAddressMap[service.accountAddressId] + : null} + handleViewCompletedSession(service) + }} + /> + {/each} +
+ + {:else} +
+

No completed services found.

+
+ {/if} + {/if} +
+ {:else} +
+

No services found.

+
+ {/if} +
+
diff --git a/src/routes/services/+page.ts b/src/routes/services/+page.ts new file mode 100644 index 0000000..72ab5fe --- /dev/null +++ b/src/routes/services/+page.ts @@ -0,0 +1,130 @@ +import { ServiceChoices, DateOrdering, type ServiceFilter } from '$houdini'; +import { + load_GetServicesByTeamMember, + load_AccountToAddressMap, + load_GetAccountAddress, + load_ActiveServiceSession, + load_LatestCompletedServiceSession +} from '$houdini'; +import { toGlobalId } from '$lib/utils/relay'; +import { error } from '@sveltejs/kit'; + +const PAGE_SIZE = 20; + +export const load = async (event) => { + // Get session from parent layout + const { session } = await event.parent(); + + if (!session?.identity?.metadata_public?.django_profile_id) { + throw error(400, 'No profile ID found in session'); + } + + // Pass the UUID directly - backend expects raw UUID, not global ID + const teamProfileId = session.identity.metadata_public.django_profile_id; + + // Get month and year from URL params, default to the current month/year + const now = new Date(); + const month = event.url.searchParams.get('month') || String(now.getMonth() + 1); + const year = event.url.searchParams.get('year') || String(now.getFullYear()); + + // Base date filter (always filter by month/year) + const dateFilter = { + month: { exact: parseInt(month) }, + year: { exact: parseInt(year) } + }; + + // Scheduled services filter: status NOT in [COMPLETED, IN_PROGRESS], ascending order + const scheduledFilters: ServiceFilter = { + date: dateFilter, + status: { inList: [ServiceChoices.SCHEDULED, ServiceChoices.CANCELLED] } + }; + + // In Progress services filter: status = IN_PROGRESS (no pagination needed) + const inProgressFilters: ServiceFilter = { + date: dateFilter, + status: { exact: ServiceChoices.IN_PROGRESS } + }; + + // Completed services filter: status = COMPLETED, descending order + const completedFilters: ServiceFilter = { + date: dateFilter, + status: { exact: ServiceChoices.COMPLETED } + }; + + // Load services with per-tab filters and pagination (initial page only) + const [scheduledServices, inProgressServices, completedServices] = await Promise.all([ + load_GetServicesByTeamMember({ + event, + variables: { + teamProfileId, + first: PAGE_SIZE, + filters: scheduledFilters, + ordering: DateOrdering.ASC + } + }), + load_GetServicesByTeamMember({ + event, + variables: { + teamProfileId, + filters: inProgressFilters, + ordering: DateOrdering.ASC + } + }), + load_GetServicesByTeamMember({ + event, + variables: { + teamProfileId, + first: PAGE_SIZE, + filters: completedFilters, + ordering: DateOrdering.DESC + } + }) + ]); + + // Load account to the address map + const accountToAddressMap = await load_AccountToAddressMap({ event }); + + // Load address if accountAddressId is in URL params + const accountAddressId = event.url.searchParams.get('address'); + let address; + if (accountAddressId) { + address = await load_GetAccountAddress({ + event, + variables: { id: toGlobalId('AccountAddressType', accountAddressId) } + }); + } + + // Load active session if serviceId is in URL params + const serviceId = event.url.searchParams.get('serviceId'); + let activeSession; + if (serviceId) { + activeSession = await load_ActiveServiceSession({ + event, + variables: { serviceId } + }); + } + + // Load latest completed session if completedServiceId is in URL params + const completedServiceId = event.url.searchParams.get('completedServiceId'); + let completedSession; + if (completedServiceId) { + completedSession = await load_LatestCompletedServiceSession({ + event, + variables: { serviceId: completedServiceId } + }); + } + + return { + scheduledServices, + inProgressServices, + completedServices, + accountToAddressMap, + address, + activeSession, + completedSession, + // Pass these for client-side load more + teamProfileId, + dateFilter, + pageSize: PAGE_SIZE + }; +}; diff --git a/src/routes/services/[service]/+page.svelte b/src/routes/services/[service]/+page.svelte new file mode 100644 index 0000000..fc2d509 --- /dev/null +++ b/src/routes/services/[service]/+page.svelte @@ -0,0 +1,384 @@ + + + +
+ {#if loading} +
+
+
+

Loading service details...

+
+
+ {:else if navigatingToSession} +
+
+
+

Opening session...

+
+
+ {:else if error} +
+
+ + + +
+

Error Loading Service

+

{error?.[0]?.message}

+
+
+
+ {:else if $service.data?.service} + {@const svc = $service.data.service} + + + + + +
+

+ Service {formatDate(svc.date)} +

+
+ +
+ +
+ +
+
+

Service Information

+ + + {#if svc.status === 'IN_PROGRESS'} + + {:else if svc.status === 'COMPLETED'} + + {/if} +
+ +
+ + {#if svc.accountAddressId && accountAddressMap[svc.accountAddressId]} +
+

Account

+

+ {accountAddressMap[svc.accountAddressId]} +

+
+ {/if} + + +
+
+

Date

+

+ {formatDate(svc.date)} +

+
+
+

Status

+ +
+ {#if $addressData?.data?.accountAddress?.labors?.length} + {@const currentLabor = + $addressData.data.accountAddress.labors.find((l) => { + const today = new Date().toISOString().split('T')[0]; + const startOk = !l.startDate || l.startDate <= today; + const endOk = !l.endDate || l.endDate >= today; + return startOk && endOk; + }) || $addressData.data.accountAddress.labors[0]} +
+

Labor

+

+ ${currentLabor.amount ? parseFloat(currentLabor.amount).toFixed(2) : '0.00'} +

+
+ {/if} +
+ + + = 2 ? getGroupMessageUrl() : undefined} + /> + + + {#if svc.notes} +
+

Notes

+

+ {svc.notes} +

+
+ {/if} +
+
+ + + {#if svc.accountAddressId && addressData && $addressData?.data?.accountAddress} + {@const addr = $addressData.data.accountAddress} +
+

Service Address

+ +
+ {#if addr.name} +

{addr.name}

+ {/if} +

{addr.streetAddress}

+

+ {addr.city}, {addr.state} + {addr.zipCode} +

+
+ + + +
+ {/if} +
+ + + +
+ {:else} +
+

Service not found.

+
+ {/if} +
+
diff --git a/src/routes/services/[service]/+page.ts b/src/routes/services/[service]/+page.ts new file mode 100644 index 0000000..bcde27e --- /dev/null +++ b/src/routes/services/[service]/+page.ts @@ -0,0 +1,58 @@ +import { + load_GetAccountAddress, + load_GetService, + load_GetTeamProfiles, + load_AccountToAddressMap, + load_ActiveServiceSession, + load_LatestCompletedServiceSession +} from '$houdini'; +import { toGlobalId } from '$lib/utils/relay'; + +export const load = async (event) => { + const service = await load_GetService({ event, variables: { id: event.params.service } }); + + // Load address if accountAddressId is in URL params + const accountAddressId = event.url.searchParams.get('addressId'); + let address; + if (accountAddressId) { + address = await load_GetAccountAddress({ + event, + variables: { id: toGlobalId('AccountAddressType', accountAddressId) } + }); + } + + // Load team members + const teamProfiles = await load_GetTeamProfiles({ event }); + + // Load account to address map + const accountToAddressMap = await load_AccountToAddressMap({ event }); + + // Load active session if openSession param is set (for IN_PROGRESS services) + const openSessionId = event.url.searchParams.get('openSession'); + let activeSession; + if (openSessionId) { + activeSession = await load_ActiveServiceSession({ + event, + variables: { serviceId: openSessionId } + }); + } + + // Load latest completed session if viewSession param is set (for COMPLETED services) + const viewSessionId = event.url.searchParams.get('viewSession'); + let completedSession; + if (viewSessionId) { + completedSession = await load_LatestCompletedServiceSession({ + event, + variables: { serviceId: viewSessionId } + }); + } + + return { + ...service, + address, + teamProfiles, + accountToAddressMap, + activeSession, + completedSession + }; +}; diff --git a/src/routes/sessions/projects/[session]/+page.svelte b/src/routes/sessions/projects/[session]/+page.svelte new file mode 100644 index 0000000..7e6cf31 --- /dev/null +++ b/src/routes/sessions/projects/[session]/+page.svelte @@ -0,0 +1,492 @@ + + + + {#if loading} +
+
+
+ Loading... +
+
+ {:else if error} +
+
+

Error loading session

+

+ {getErrorMessage(error)} +

+
+
+ {:else if session} + 0} + {isSubmitting} + onClose={handleCloseSession} + /> + + (activeTab = tab)} + /> + + {#if activeTab === 'summary'} + + {:else if activeTab === 'tasks'} + selectedTaskIds.clear()} + /> + {:else if activeTab === 'media'} + + {:else if activeTab === 'notes'} + + {/if} + {:else} +
+
+

Session not found

+
+
+ {/if} +
diff --git a/src/routes/sessions/projects/[session]/+page.ts b/src/routes/sessions/projects/[session]/+page.ts new file mode 100644 index 0000000..d330a32 --- /dev/null +++ b/src/routes/sessions/projects/[session]/+page.ts @@ -0,0 +1,63 @@ +import { + load_GetProjectSession, + load_GetTeamProfiles, + load_GetProject, + load_AccountToAddressMap, + load_GetProjectScope, + load_GetCustomer +} from '$houdini'; +import { toGlobalId } from '$lib/utils/relay'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async (event) => { + const { session } = event.params; + const projectId = event.url.searchParams.get('projectId'); + const scopeId = event.url.searchParams.get('scopeId'); + const customerId = event.url.searchParams.get('customerId'); + + // Load base queries in parallel + const [sessionResult, teamMembersResult, accountsResult] = await Promise.all([ + load_GetProjectSession({ + event, + variables: { id: session } + }), + load_GetTeamProfiles({ event }), + load_AccountToAddressMap({ event }) + ]); + + // Optionally load project if projectId is provided + let projectResult; + if (projectId) { + projectResult = await load_GetProject({ + event, + variables: { id: toGlobalId('ProjectType', projectId) } + }); + } + + // Load scope if scopeId is provided + let scopeResult; + if (scopeId) { + scopeResult = await load_GetProjectScope({ + event, + variables: { id: toGlobalId('ProjectScopeType', scopeId) } + }); + } + + // Load customer if customerId is provided + let customerResult; + if (customerId) { + customerResult = await load_GetCustomer({ + event, + variables: { id: toGlobalId('CustomerType', customerId) } + }); + } + + return { + ...sessionResult, + ...teamMembersResult, + ...accountsResult, + ...(projectResult || {}), + ...(scopeResult || {}), + ...(customerResult || {}) + }; +}; diff --git a/src/routes/sessions/services/[session]/+page.svelte b/src/routes/sessions/services/[session]/+page.svelte new file mode 100644 index 0000000..7c17743 --- /dev/null +++ b/src/routes/sessions/services/[session]/+page.svelte @@ -0,0 +1,477 @@ + + + + {#if loading} +
+
+
+ Loading... +
+
+ {:else if error} +
+
+

Error loading session

+

+ {getErrorMessage(error)} +

+
+
+ {:else if session} + 0} + {isSubmitting} + onClose={handleCloseSession} + /> + + (activeTab = tab)} + /> + + {#if activeTab === 'summary'} + + {:else if activeTab === 'tasks'} + selectedTaskIds.clear()} + /> + {:else if activeTab === 'media'} + + {:else if activeTab === 'notes'} + + {/if} + {:else} +
+
+

Session not found

+
+
+ {/if} +
diff --git a/src/routes/sessions/services/[session]/+page.ts b/src/routes/sessions/services/[session]/+page.ts new file mode 100644 index 0000000..665b858 --- /dev/null +++ b/src/routes/sessions/services/[session]/+page.ts @@ -0,0 +1,63 @@ +import { + load_GetServiceSession, + load_GetTeamProfiles, + load_GetService, + load_AccountToAddressMap, + load_GetScope, + load_GetCustomer +} from '$houdini'; +import { toGlobalId } from '$lib/utils/relay'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async (event) => { + const { session } = event.params; + const serviceId = event.url.searchParams.get('serviceId'); + const scopeId = event.url.searchParams.get('scopeId'); + const customerId = event.url.searchParams.get('customerId'); + + // Load base queries in parallel + const [sessionResult, teamMembersResult, accountsResult] = await Promise.all([ + load_GetServiceSession({ + event, + variables: { id: session } + }), + load_GetTeamProfiles({ event }), + load_AccountToAddressMap({ event }) + ]); + + // Optionally load service if serviceId is provided + let serviceResult; + if (serviceId) { + serviceResult = await load_GetService({ + event, + variables: { id: toGlobalId('ServiceType', serviceId) } + }); + } + + // Load scope if scopeId is provided + let scopeResult; + if (scopeId) { + scopeResult = await load_GetScope({ + event, + variables: { id: toGlobalId('ScopeType', scopeId) } + }); + } + + // Load customer if customerId is provided + let customerResult; + if (customerId) { + customerResult = await load_GetCustomer({ + event, + variables: { id: toGlobalId('CustomerType', customerId) } + }); + } + + return { + ...sessionResult, + ...teamMembersResult, + ...accountsResult, + ...(serviceResult || {}), + ...(scopeResult || {}), + ...(customerResult || {}) + }; +}; diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..b113b00 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + + alias: { + $houdini: '.houdini/' + } + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..338445d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "rootDirs": [".", "./.svelte-kit/types", "./.houdini/types"] + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0c3061b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,13 @@ +import houdini from 'houdini/vite'; +import devtoolsJson from 'vite-plugin-devtools-json'; +import tailwindcss from '@tailwindcss/vite'; +import mkcert from 'vite-plugin-mkcert'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + host: 'local.example.com' + }, + plugins: [houdini(), tailwindcss(), sveltekit(), devtoolsJson(), mkcert()] +});