mvc-kit 2.12.5 → 2.13.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/BEST_PRACTICES.md +1390 -0
- package/agent-config/bin/postinstall.mjs +4 -3
- package/agent-config/bin/setup.mjs +5 -1
- package/agent-config/claude-code/agents/mvc-kit-architect.md +16 -8
- package/agent-config/claude-code/skills/guide/SKILL.md +29 -7
- package/agent-config/claude-code/skills/guide/patterns.md +12 -0
- package/agent-config/claude-code/skills/guide/recipes.md +510 -0
- package/agent-config/claude-code/skills/guide/testing.md +297 -0
- package/agent-config/claude-code/skills/review/SKILL.md +3 -13
- package/agent-config/claude-code/skills/review/checklist.md +30 -5
- package/agent-config/claude-code/skills/scaffold/SKILL.md +4 -13
- package/agent-config/lib/install-claude.mjs +90 -25
- package/dist/Channel.cjs +276 -300
- package/dist/Channel.cjs.map +1 -1
- package/dist/Channel.js +275 -299
- package/dist/Channel.js.map +1 -1
- package/dist/Collection.cjs +424 -504
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.js +423 -503
- package/dist/Collection.js.map +1 -1
- package/dist/Controller.cjs +70 -67
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.js +69 -66
- package/dist/Controller.js.map +1 -1
- package/dist/EventBus.cjs +77 -88
- package/dist/EventBus.cjs.map +1 -1
- package/dist/EventBus.js +76 -87
- package/dist/EventBus.js.map +1 -1
- package/dist/Feed.cjs +81 -77
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.js +80 -76
- package/dist/Feed.js.map +1 -1
- package/dist/Model.cjs +181 -207
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.js +179 -205
- package/dist/Model.js.map +1 -1
- package/dist/Pagination.cjs +75 -73
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.js +74 -72
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +255 -287
- package/dist/Pending.cjs.map +1 -1
- package/dist/Pending.js +253 -285
- package/dist/Pending.js.map +1 -1
- package/dist/PersistentCollection.cjs +242 -285
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.js +241 -284
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +166 -174
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.js +164 -172
- package/dist/Resource.js.map +1 -1
- package/dist/Selection.cjs +84 -94
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.js +83 -93
- package/dist/Selection.js.map +1 -1
- package/dist/Service.cjs +54 -55
- package/dist/Service.cjs.map +1 -1
- package/dist/Service.js +53 -54
- package/dist/Service.js.map +1 -1
- package/dist/Sorting.cjs +102 -101
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.js +102 -101
- package/dist/Sorting.js.map +1 -1
- package/dist/Trackable.cjs +112 -80
- package/dist/Trackable.cjs.map +1 -1
- package/dist/Trackable.js +111 -79
- package/dist/Trackable.js.map +1 -1
- package/dist/ViewModel.cjs +528 -576
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.js +525 -573
- package/dist/ViewModel.js.map +1 -1
- package/dist/bindPublicMethods.cjs +43 -24
- package/dist/bindPublicMethods.cjs.map +1 -1
- package/dist/bindPublicMethods.js +43 -24
- package/dist/bindPublicMethods.js.map +1 -1
- package/dist/errors.cjs +67 -68
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +68 -71
- package/dist/errors.js.map +1 -1
- package/dist/mvc-kit.cjs +44 -46
- package/dist/mvc-kit.js +5 -32
- package/dist/produceDraft.cjs +105 -95
- package/dist/produceDraft.cjs.map +1 -1
- package/dist/produceDraft.js +106 -97
- package/dist/produceDraft.js.map +1 -1
- package/dist/react/components/CardList.cjs +30 -40
- package/dist/react/components/CardList.cjs.map +1 -1
- package/dist/react/components/CardList.js +31 -41
- package/dist/react/components/CardList.js.map +1 -1
- package/dist/react/components/DataTable.cjs +146 -169
- package/dist/react/components/DataTable.cjs.map +1 -1
- package/dist/react/components/DataTable.js +147 -170
- package/dist/react/components/DataTable.js.map +1 -1
- package/dist/react/components/InfiniteScroll.cjs +51 -42
- package/dist/react/components/InfiniteScroll.cjs.map +1 -1
- package/dist/react/components/InfiniteScroll.js +52 -43
- package/dist/react/components/InfiniteScroll.js.map +1 -1
- package/dist/react/components/types.cjs +10 -6
- package/dist/react/components/types.cjs.map +1 -1
- package/dist/react/components/types.js +11 -9
- package/dist/react/components/types.js.map +1 -1
- package/dist/react/guards.cjs +10 -6
- package/dist/react/guards.cjs.map +1 -1
- package/dist/react/guards.js +11 -9
- package/dist/react/guards.js.map +1 -1
- package/dist/react/provider.cjs +23 -20
- package/dist/react/provider.cjs.map +1 -1
- package/dist/react/provider.js +23 -21
- package/dist/react/provider.js.map +1 -1
- package/dist/react/use-event-bus.cjs +24 -20
- package/dist/react/use-event-bus.cjs.map +1 -1
- package/dist/react/use-event-bus.js +24 -21
- package/dist/react/use-event-bus.js.map +1 -1
- package/dist/react/use-instance.cjs +43 -36
- package/dist/react/use-instance.cjs.map +1 -1
- package/dist/react/use-instance.js +43 -36
- package/dist/react/use-instance.js.map +1 -1
- package/dist/react/use-local.cjs +48 -64
- package/dist/react/use-local.cjs.map +1 -1
- package/dist/react/use-local.js +47 -63
- package/dist/react/use-local.js.map +1 -1
- package/dist/react/use-model.cjs +84 -98
- package/dist/react/use-model.cjs.map +1 -1
- package/dist/react/use-model.js +84 -100
- package/dist/react/use-model.js.map +1 -1
- package/dist/react/use-singleton.cjs +19 -23
- package/dist/react/use-singleton.cjs.map +1 -1
- package/dist/react/use-singleton.js +16 -20
- package/dist/react/use-singleton.js.map +1 -1
- package/dist/react/use-subscribe-only.cjs +28 -22
- package/dist/react/use-subscribe-only.cjs.map +1 -1
- package/dist/react/use-subscribe-only.js +28 -22
- package/dist/react/use-subscribe-only.js.map +1 -1
- package/dist/react/use-teardown.cjs +20 -19
- package/dist/react/use-teardown.cjs.map +1 -1
- package/dist/react/use-teardown.js +20 -19
- package/dist/react/use-teardown.js.map +1 -1
- package/dist/react-native/NativeCollection.cjs +98 -78
- package/dist/react-native/NativeCollection.cjs.map +1 -1
- package/dist/react-native/NativeCollection.js +97 -77
- package/dist/react-native/NativeCollection.js.map +1 -1
- package/dist/react-native.cjs +2 -4
- package/dist/react-native.js +1 -4
- package/dist/react.cjs +24 -26
- package/dist/react.js +1 -17
- package/dist/singleton.cjs +28 -22
- package/dist/singleton.cjs.map +1 -1
- package/dist/singleton.js +29 -26
- package/dist/singleton.js.map +1 -1
- package/dist/walkPrototypeChain.cjs +20 -12
- package/dist/walkPrototypeChain.cjs.map +1 -1
- package/dist/walkPrototypeChain.js +21 -13
- package/dist/walkPrototypeChain.js.map +1 -1
- package/dist/web/IndexedDBCollection.cjs +53 -36
- package/dist/web/IndexedDBCollection.cjs.map +1 -1
- package/dist/web/IndexedDBCollection.js +52 -35
- package/dist/web/IndexedDBCollection.js.map +1 -1
- package/dist/web/WebStorageCollection.cjs +82 -84
- package/dist/web/WebStorageCollection.cjs.map +1 -1
- package/dist/web/WebStorageCollection.js +81 -83
- package/dist/web/WebStorageCollection.js.map +1 -1
- package/dist/web/idb.cjs +107 -99
- package/dist/web/idb.cjs.map +1 -1
- package/dist/web/idb.js +108 -105
- package/dist/web/idb.js.map +1 -1
- package/dist/web.cjs +4 -6
- package/dist/web.js +1 -5
- package/dist/wrapAsyncMethods.cjs +141 -168
- package/dist/wrapAsyncMethods.cjs.map +1 -1
- package/dist/wrapAsyncMethods.js +141 -168
- package/dist/wrapAsyncMethods.js.map +1 -1
- package/examples/primitive/channel.ts +109 -0
- package/examples/primitive/collection.ts +118 -0
- package/examples/primitive/controller.ts +118 -0
- package/examples/primitive/counter.ts +108 -0
- package/examples/primitive/env.d.ts +1 -0
- package/examples/primitive/eventbus.ts +77 -0
- package/examples/primitive/feed.ts +162 -0
- package/examples/primitive/model.ts +82 -0
- package/examples/primitive/pagination.ts +91 -0
- package/examples/primitive/pending.ts +189 -0
- package/examples/primitive/persistent-collection.ts +116 -0
- package/examples/primitive/resource.ts +114 -0
- package/examples/primitive/selection.ts +96 -0
- package/examples/primitive/sorting.ts +112 -0
- package/examples/primitive/timer.ts +58 -0
- package/examples/primitive/trackable.ts +225 -0
- package/examples/primitive/tsconfig.json +20 -0
- package/examples/primitive/viewmodel-service.ts +161 -0
- package/examples/react/AuthExample/index.html +12 -0
- package/examples/react/AuthExample/src/App.tsx +29 -0
- package/examples/react/AuthExample/src/components/AdminPage.tsx +51 -0
- package/examples/react/AuthExample/src/components/AppHeader.tsx +32 -0
- package/examples/react/AuthExample/src/components/AuthGuard.tsx +50 -0
- package/examples/react/AuthExample/src/components/AuthScreen.tsx +181 -0
- package/examples/react/AuthExample/src/components/DashboardPage.tsx +41 -0
- package/examples/react/AuthExample/src/components/ProfilePage.tsx +44 -0
- package/examples/react/AuthExample/src/components/Toast.tsx +41 -0
- package/examples/react/AuthExample/src/env.d.ts +10 -0
- package/examples/react/AuthExample/src/events/AppEventBus.ts +7 -0
- package/examples/react/AuthExample/src/main.tsx +10 -0
- package/examples/react/AuthExample/src/mock/api.ts +78 -0
- package/examples/react/AuthExample/src/models/LoginFormModel.ts +19 -0
- package/examples/react/AuthExample/src/models/RegisterFormModel.ts +25 -0
- package/examples/react/AuthExample/src/services/AuthService.ts +21 -0
- package/examples/react/AuthExample/src/styles.css +445 -0
- package/examples/react/AuthExample/src/types/auth.ts +12 -0
- package/examples/react/AuthExample/src/viewmodels/AuthViewModel.ts +111 -0
- package/examples/react/AuthExample/tsconfig.json +22 -0
- package/examples/react/AuthExample/vite.config.ts +18 -0
- package/examples/react/ComplexApp/index.html +12 -0
- package/examples/react/ComplexApp/src/App.tsx +17 -0
- package/examples/react/ComplexApp/src/channels/ActivityChannel.ts +24 -0
- package/examples/react/ComplexApp/src/channels/DashboardChannel.ts +26 -0
- package/examples/react/ComplexApp/src/channels/ErrorsChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/LatencyChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/OrdersChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/RevenueChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/TrafficChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/UsersMetricChannel.ts +5 -0
- package/examples/react/ComplexApp/src/collections/DashboardCollection.ts +6 -0
- package/examples/react/ComplexApp/src/collections/ErrorsCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/LatencyCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/OrdersCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/RevenueCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/TrafficCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/UsersMetricCollection.ts +3 -0
- package/examples/react/ComplexApp/src/components/activity/ActivityFeed.tsx +31 -0
- package/examples/react/ComplexApp/src/components/activity/ActivityItemRow.tsx +35 -0
- package/examples/react/ComplexApp/src/components/dashboard/DashboardCard.tsx +37 -0
- package/examples/react/ComplexApp/src/components/dashboard/DashboardPage.tsx +34 -0
- package/examples/react/ComplexApp/src/components/layout/Navbar.tsx +32 -0
- package/examples/react/ComplexApp/src/components/layout/SocialFeedPanel.tsx +57 -0
- package/examples/react/ComplexApp/src/components/shared/Spinner.tsx +3 -0
- package/examples/react/ComplexApp/src/components/shared/StatusIndicator.tsx +13 -0
- package/examples/react/ComplexApp/src/components/shared/Toast.tsx +40 -0
- package/examples/react/ComplexApp/src/env.d.ts +10 -0
- package/examples/react/ComplexApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/ComplexApp/src/main.tsx +10 -0
- package/examples/react/ComplexApp/src/mock-remote/MockWebSocket.ts +38 -0
- package/examples/react/ComplexApp/src/mock-remote/activity-api.ts +48 -0
- package/examples/react/ComplexApp/src/mock-remote/dashboard-generators.ts +45 -0
- package/examples/react/ComplexApp/src/mock-remote/delay.ts +18 -0
- package/examples/react/ComplexApp/src/mock-remote/social-api.ts +55 -0
- package/examples/react/ComplexApp/src/resources/ActivityResource.ts +12 -0
- package/examples/react/ComplexApp/src/resources/SocialFeedResource.ts +17 -0
- package/examples/react/ComplexApp/src/styles.css +463 -0
- package/examples/react/ComplexApp/src/types/activity.ts +8 -0
- package/examples/react/ComplexApp/src/types/dashboard.ts +5 -0
- package/examples/react/ComplexApp/src/types/social.ts +8 -0
- package/examples/react/ComplexApp/src/types/users.ts +6 -0
- package/examples/react/ComplexApp/src/viewmodels/ActivityFeedViewModel.ts +68 -0
- package/examples/react/ComplexApp/src/viewmodels/AppStateViewModel.ts +26 -0
- package/examples/react/ComplexApp/src/viewmodels/DashboardCardViewModel.ts +69 -0
- package/examples/react/ComplexApp/src/viewmodels/ErrorsCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/LatencyCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/OrdersCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/RevenueCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/SocialFeedViewModel.ts +39 -0
- package/examples/react/ComplexApp/src/viewmodels/TrafficCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/UsersMetricCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/tsconfig.json +22 -0
- package/examples/react/ComplexApp/vite.config.ts +18 -0
- package/examples/react/FullApp/index.html +12 -0
- package/examples/react/FullApp/src/App.tsx +28 -0
- package/examples/react/FullApp/src/collections/ConversationsCollection.ts +4 -0
- package/examples/react/FullApp/src/collections/LocationsCollection.ts +4 -0
- package/examples/react/FullApp/src/components/auth/LoginPage.tsx +80 -0
- package/examples/react/FullApp/src/components/dashboard/DashboardPage.tsx +29 -0
- package/examples/react/FullApp/src/components/dashboard/RecentActivityCard.tsx +35 -0
- package/examples/react/FullApp/src/components/dashboard/StatsCard.tsx +19 -0
- package/examples/react/FullApp/src/components/layout/AppShell.tsx +31 -0
- package/examples/react/FullApp/src/components/layout/Header.tsx +25 -0
- package/examples/react/FullApp/src/components/layout/Sidebar.tsx +29 -0
- package/examples/react/FullApp/src/components/locations/LocationFilters.tsx +60 -0
- package/examples/react/FullApp/src/components/locations/LocationForm.tsx +112 -0
- package/examples/react/FullApp/src/components/locations/LocationProfilePage.tsx +81 -0
- package/examples/react/FullApp/src/components/locations/LocationsPage.tsx +127 -0
- package/examples/react/FullApp/src/components/messaging/ConversationList.tsx +59 -0
- package/examples/react/FullApp/src/components/messaging/MessageBubble.tsx +22 -0
- package/examples/react/FullApp/src/components/messaging/MessageThread.tsx +100 -0
- package/examples/react/FullApp/src/components/messaging/MessagingPage.tsx +52 -0
- package/examples/react/FullApp/src/components/shared/ErrorBanner.tsx +3 -0
- package/examples/react/FullApp/src/components/shared/Spinner.tsx +7 -0
- package/examples/react/FullApp/src/components/shared/Toast.tsx +41 -0
- package/examples/react/FullApp/src/components/users/UserFilters.tsx +59 -0
- package/examples/react/FullApp/src/components/users/UsersPage.tsx +80 -0
- package/examples/react/FullApp/src/components/users/UsersTable.tsx +52 -0
- package/examples/react/FullApp/src/env.d.ts +10 -0
- package/examples/react/FullApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/FullApp/src/main.tsx +10 -0
- package/examples/react/FullApp/src/mock/delay.ts +21 -0
- package/examples/react/FullApp/src/mock/locations.ts +76 -0
- package/examples/react/FullApp/src/mock/messages.ts +237 -0
- package/examples/react/FullApp/src/mock/users.ts +84 -0
- package/examples/react/FullApp/src/models/LocationFormModel.ts +31 -0
- package/examples/react/FullApp/src/models/LoginFormModel.ts +19 -0
- package/examples/react/FullApp/src/resources/UsersResource.ts +12 -0
- package/examples/react/FullApp/src/services/AuthService.ts +18 -0
- package/examples/react/FullApp/src/services/LocationService.ts +23 -0
- package/examples/react/FullApp/src/services/MessageService.ts +65 -0
- package/examples/react/FullApp/src/services/UserService.ts +23 -0
- package/examples/react/FullApp/src/styles.css +767 -0
- package/examples/react/FullApp/src/types/conversation.ts +7 -0
- package/examples/react/FullApp/src/types/location.ts +12 -0
- package/examples/react/FullApp/src/types/message.ts +7 -0
- package/examples/react/FullApp/src/types/user.ts +10 -0
- package/examples/react/FullApp/src/viewmodels/AuthViewModel.ts +51 -0
- package/examples/react/FullApp/src/viewmodels/ConversationsViewModel.ts +89 -0
- package/examples/react/FullApp/src/viewmodels/DashboardViewModel.ts +56 -0
- package/examples/react/FullApp/src/viewmodels/LocationProfileViewModel.ts +81 -0
- package/examples/react/FullApp/src/viewmodels/LocationsViewModel.ts +113 -0
- package/examples/react/FullApp/src/viewmodels/MessageThreadViewModel.ts +83 -0
- package/examples/react/FullApp/src/viewmodels/UsersViewModel.ts +88 -0
- package/examples/react/FullApp/tsconfig.json +22 -0
- package/examples/react/FullApp/vite.config.ts +18 -0
- package/examples/react/WorkerApp/index.html +12 -0
- package/examples/react/WorkerApp/src/App.tsx +24 -0
- package/examples/react/WorkerApp/src/channels/MessagingChannel.ts +46 -0
- package/examples/react/WorkerApp/src/channels/WorkerStatusChannel.ts +35 -0
- package/examples/react/WorkerApp/src/components/auth/LoginPage.tsx +60 -0
- package/examples/react/WorkerApp/src/components/layout/AppShell.tsx +31 -0
- package/examples/react/WorkerApp/src/components/layout/Header.tsx +23 -0
- package/examples/react/WorkerApp/src/components/layout/Sidebar.tsx +28 -0
- package/examples/react/WorkerApp/src/components/messaging/ComposeBar.tsx +33 -0
- package/examples/react/WorkerApp/src/components/messaging/ConversationList.tsx +59 -0
- package/examples/react/WorkerApp/src/components/messaging/MessageBubble.tsx +45 -0
- package/examples/react/WorkerApp/src/components/messaging/MessageThread.tsx +93 -0
- package/examples/react/WorkerApp/src/components/messaging/MessagingPage.tsx +53 -0
- package/examples/react/WorkerApp/src/components/shared/ErrorBanner.tsx +3 -0
- package/examples/react/WorkerApp/src/components/shared/PendingBanner.tsx +37 -0
- package/examples/react/WorkerApp/src/components/shared/Spinner.tsx +7 -0
- package/examples/react/WorkerApp/src/components/shared/Toast.tsx +41 -0
- package/examples/react/WorkerApp/src/components/shift/ShiftPage.tsx +98 -0
- package/examples/react/WorkerApp/src/components/shift/ShiftTimer.tsx +24 -0
- package/examples/react/WorkerApp/src/components/shift/SiteSelector.tsx +27 -0
- package/examples/react/WorkerApp/src/components/sites/SiteFilters.tsx +61 -0
- package/examples/react/WorkerApp/src/components/sites/SitesPage.tsx +102 -0
- package/examples/react/WorkerApp/src/env.d.ts +10 -0
- package/examples/react/WorkerApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/WorkerApp/src/main.tsx +10 -0
- package/examples/react/WorkerApp/src/mock/MockWebSocket.ts +38 -0
- package/examples/react/WorkerApp/src/mock/delay.ts +31 -0
- package/examples/react/WorkerApp/src/mock/messages.ts +120 -0
- package/examples/react/WorkerApp/src/mock/shifts.ts +57 -0
- package/examples/react/WorkerApp/src/mock/sites.ts +14 -0
- package/examples/react/WorkerApp/src/mock/workers.ts +12 -0
- package/examples/react/WorkerApp/src/models/ComposeMessageModel.ts +17 -0
- package/examples/react/WorkerApp/src/resources/ConversationsResource.ts +10 -0
- package/examples/react/WorkerApp/src/resources/MessagesResource.ts +32 -0
- package/examples/react/WorkerApp/src/resources/ShiftResource.ts +73 -0
- package/examples/react/WorkerApp/src/resources/SitesResource.ts +11 -0
- package/examples/react/WorkerApp/src/resources/WorkersResource.ts +11 -0
- package/examples/react/WorkerApp/src/styles.css +756 -0
- package/examples/react/WorkerApp/src/types/conversation.ts +7 -0
- package/examples/react/WorkerApp/src/types/message.ts +7 -0
- package/examples/react/WorkerApp/src/types/shift.ts +13 -0
- package/examples/react/WorkerApp/src/types/site.ts +8 -0
- package/examples/react/WorkerApp/src/types/worker.ts +8 -0
- package/examples/react/WorkerApp/src/viewmodels/AuthViewModel.ts +41 -0
- package/examples/react/WorkerApp/src/viewmodels/ConversationsViewModel.ts +83 -0
- package/examples/react/WorkerApp/src/viewmodels/MessageThreadViewModel.ts +113 -0
- package/examples/react/WorkerApp/src/viewmodels/ShiftViewModel.ts +147 -0
- package/examples/react/WorkerApp/src/viewmodels/SitesViewModel.ts +82 -0
- package/examples/react/WorkerApp/tsconfig.json +22 -0
- package/examples/react/WorkerApp/vite.config.ts +18 -0
- package/package.json +11 -9
- package/src/Pending.test.ts +1 -2
- package/src/Sorting.test.ts +1 -1
- package/src/produceDraft.test.ts +3 -3
- package/src/react/components/CardList.test.tsx +1 -1
- package/src/react/components/DataTable.test.tsx +1 -1
- package/src/react/components/InfiniteScroll.test.tsx +5 -5
- package/dist/mvc-kit.cjs.map +0 -1
- package/dist/mvc-kit.js.map +0 -1
- package/dist/react-native.cjs.map +0 -1
- package/dist/react-native.js.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/web.cjs.map +0 -1
- package/dist/web.js.map +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useLocal } from 'mvc-kit/react';
|
|
2
|
+
import { ShiftViewModel } from '../../viewmodels/ShiftViewModel';
|
|
3
|
+
import { ShiftTimer } from './ShiftTimer';
|
|
4
|
+
import { SiteSelector } from './SiteSelector';
|
|
5
|
+
import { PendingBanner } from '../shared/PendingBanner';
|
|
6
|
+
import { Spinner } from '../shared/Spinner';
|
|
7
|
+
|
|
8
|
+
export function ShiftPage() {
|
|
9
|
+
const [state, vm] = useLocal(ShiftViewModel, { selectedSiteId: null, now: Date.now() });
|
|
10
|
+
const { loading } = vm.async.load;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<h1 className="page-title">My Shift</h1>
|
|
15
|
+
|
|
16
|
+
<PendingBanner
|
|
17
|
+
pending={vm.pending}
|
|
18
|
+
label="shift operations"
|
|
19
|
+
renderEntry={entry => (
|
|
20
|
+
<span>{entry.meta?.action} at {entry.meta?.siteName} — {entry.error}</span>
|
|
21
|
+
)}
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
{loading && <Spinner />}
|
|
25
|
+
|
|
26
|
+
{!loading && !vm.isOnShift && (
|
|
27
|
+
<div className="card">
|
|
28
|
+
<h2 style={{ marginBottom: '1rem' }}>Clock In</h2>
|
|
29
|
+
<p style={{ color: 'var(--color-text-secondary)', marginBottom: '1rem' }}>
|
|
30
|
+
Select a work site and clock in to start your shift.
|
|
31
|
+
</p>
|
|
32
|
+
<SiteSelector
|
|
33
|
+
sites={vm.availableSites}
|
|
34
|
+
selectedId={state.selectedSiteId}
|
|
35
|
+
onSelect={id => vm.selectSite(id)}
|
|
36
|
+
/>
|
|
37
|
+
<button
|
|
38
|
+
className="btn btn-primary"
|
|
39
|
+
disabled={!state.selectedSiteId || vm.hasPendingOps}
|
|
40
|
+
onClick={() => vm.clockIn()}
|
|
41
|
+
>
|
|
42
|
+
{vm.hasPendingOps ? 'Processing...' : 'Clock In'}
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{!loading && vm.isOnShift && (
|
|
48
|
+
<div className="card">
|
|
49
|
+
<ShiftTimer
|
|
50
|
+
shiftTime={vm.formattedShiftTime}
|
|
51
|
+
breakTime={vm.formattedBreakTime}
|
|
52
|
+
siteName={vm.currentSiteName}
|
|
53
|
+
isOnBreak={vm.isOnBreak}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<div className="shift-actions">
|
|
57
|
+
{vm.isOnBreak ? (
|
|
58
|
+
<button
|
|
59
|
+
className="btn btn-primary"
|
|
60
|
+
onClick={() => vm.endBreak()}
|
|
61
|
+
disabled={vm.hasPendingOps}
|
|
62
|
+
>
|
|
63
|
+
End Break
|
|
64
|
+
</button>
|
|
65
|
+
) : (
|
|
66
|
+
<button
|
|
67
|
+
className="btn btn-secondary"
|
|
68
|
+
onClick={() => vm.startBreak()}
|
|
69
|
+
disabled={vm.hasPendingOps}
|
|
70
|
+
>
|
|
71
|
+
Start Break
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<button
|
|
76
|
+
className="btn btn-danger"
|
|
77
|
+
onClick={() => vm.clockOut()}
|
|
78
|
+
disabled={vm.hasPendingOps}
|
|
79
|
+
>
|
|
80
|
+
Clock Out
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{vm.hasFailedOps && (
|
|
85
|
+
<div style={{ marginTop: '1rem' }}>
|
|
86
|
+
<button className="btn btn-sm btn-primary" onClick={() => vm.retryAll()}>
|
|
87
|
+
Retry Failed
|
|
88
|
+
</button>
|
|
89
|
+
<button className="btn btn-sm btn-secondary" style={{ marginLeft: '0.5rem' }} onClick={() => vm.dismissAll()}>
|
|
90
|
+
Dismiss
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface ShiftTimerProps {
|
|
2
|
+
shiftTime: string;
|
|
3
|
+
breakTime: string;
|
|
4
|
+
siteName: string;
|
|
5
|
+
isOnBreak: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ShiftTimer({ shiftTime, breakTime, siteName, isOnBreak }: ShiftTimerProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="shift-timer">
|
|
11
|
+
<div className="shift-timer-site">{siteName}</div>
|
|
12
|
+
<div className="shift-timer-clock">
|
|
13
|
+
<div className="timer-value">{shiftTime}</div>
|
|
14
|
+
<div className="timer-label">Shift Duration</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div className="shift-timer-break">
|
|
17
|
+
<span className={`badge ${isOnBreak ? 'badge-maintenance' : 'badge-inactive'}`}>
|
|
18
|
+
{isOnBreak ? 'On Break' : 'Working'}
|
|
19
|
+
</span>
|
|
20
|
+
<span className="timer-break-value">Break: {breakTime}</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SiteState } from '../../types/site';
|
|
2
|
+
|
|
3
|
+
interface SiteSelectorProps {
|
|
4
|
+
sites: SiteState[];
|
|
5
|
+
selectedId: string | null;
|
|
6
|
+
onSelect: (id: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SiteSelector({ sites, selectedId, onSelect }: SiteSelectorProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="form-group">
|
|
12
|
+
<label className="form-label">Work Site</label>
|
|
13
|
+
<select
|
|
14
|
+
className="form-select"
|
|
15
|
+
value={selectedId ?? ''}
|
|
16
|
+
onChange={e => onSelect(e.target.value)}
|
|
17
|
+
>
|
|
18
|
+
<option value="">Select a site...</option>
|
|
19
|
+
{sites.map(site => (
|
|
20
|
+
<option key={site.id} value={site.id}>
|
|
21
|
+
{site.name} — {site.address}
|
|
22
|
+
</option>
|
|
23
|
+
))}
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SiteState } from '../../types/site';
|
|
2
|
+
|
|
3
|
+
interface SiteFiltersProps {
|
|
4
|
+
search: string;
|
|
5
|
+
typeFilter: 'all' | SiteState['type'];
|
|
6
|
+
statusFilter: 'all' | SiteState['status'];
|
|
7
|
+
onSearchChange: (v: string) => void;
|
|
8
|
+
onTypeFilterChange: (v: 'all' | SiteState['type']) => void;
|
|
9
|
+
onStatusFilterChange: (v: 'all' | SiteState['status']) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SiteFilters({
|
|
13
|
+
search,
|
|
14
|
+
typeFilter,
|
|
15
|
+
statusFilter,
|
|
16
|
+
onSearchChange,
|
|
17
|
+
onTypeFilterChange,
|
|
18
|
+
onStatusFilterChange,
|
|
19
|
+
}: SiteFiltersProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="filters">
|
|
22
|
+
<div className="filter-group">
|
|
23
|
+
<label className="filter-label">Search</label>
|
|
24
|
+
<input
|
|
25
|
+
className="filter-input"
|
|
26
|
+
type="text"
|
|
27
|
+
value={search}
|
|
28
|
+
onChange={e => onSearchChange(e.target.value)}
|
|
29
|
+
placeholder="Search sites..."
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<div className="filter-group">
|
|
33
|
+
<label className="filter-label">Type</label>
|
|
34
|
+
<select
|
|
35
|
+
className="filter-select"
|
|
36
|
+
value={typeFilter}
|
|
37
|
+
onChange={e => onTypeFilterChange(e.target.value as any)}
|
|
38
|
+
>
|
|
39
|
+
<option value="all">All Types</option>
|
|
40
|
+
<option value="residential">Residential</option>
|
|
41
|
+
<option value="commercial">Commercial</option>
|
|
42
|
+
<option value="industrial">Industrial</option>
|
|
43
|
+
<option value="infrastructure">Infrastructure</option>
|
|
44
|
+
</select>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="filter-group">
|
|
47
|
+
<label className="filter-label">Status</label>
|
|
48
|
+
<select
|
|
49
|
+
className="filter-select"
|
|
50
|
+
value={statusFilter}
|
|
51
|
+
onChange={e => onStatusFilterChange(e.target.value as any)}
|
|
52
|
+
>
|
|
53
|
+
<option value="all">All Statuses</option>
|
|
54
|
+
<option value="active">Active</option>
|
|
55
|
+
<option value="paused">Paused</option>
|
|
56
|
+
<option value="completed">Completed</option>
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useLocal, DataTable } from 'mvc-kit/react';
|
|
2
|
+
import type { Column } from 'mvc-kit/react';
|
|
3
|
+
import { SitesViewModel } from '../../viewmodels/SitesViewModel';
|
|
4
|
+
import { SiteFilters } from './SiteFilters';
|
|
5
|
+
import { Spinner } from '../shared/Spinner';
|
|
6
|
+
import { ErrorBanner } from '../shared/ErrorBanner';
|
|
7
|
+
import type { SiteState } from '../../types/site';
|
|
8
|
+
|
|
9
|
+
const columns: Column<SiteState>[] = [
|
|
10
|
+
{
|
|
11
|
+
key: 'name',
|
|
12
|
+
header: 'Name',
|
|
13
|
+
render: site => <span style={{ fontWeight: 500 }}>{site.name}</span>,
|
|
14
|
+
sortable: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: 'type',
|
|
18
|
+
header: 'Type',
|
|
19
|
+
render: site => (
|
|
20
|
+
<span className={`badge badge-${site.type === 'residential' ? 'admin' : site.type === 'commercial' ? 'manager' : 'member'}`}>
|
|
21
|
+
{site.type}
|
|
22
|
+
</span>
|
|
23
|
+
),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'address',
|
|
27
|
+
header: 'Address',
|
|
28
|
+
render: site => site.address,
|
|
29
|
+
sortable: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'status',
|
|
33
|
+
header: 'Status',
|
|
34
|
+
render: site => <span className={`badge badge-${site.status}`}>{site.status}</span>,
|
|
35
|
+
sortable: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'workerCount',
|
|
39
|
+
header: 'Workers',
|
|
40
|
+
render: site => site.workerCount,
|
|
41
|
+
sortable: true,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function SitesPage() {
|
|
46
|
+
const [state, vm] = useLocal(SitesViewModel, {
|
|
47
|
+
search: '',
|
|
48
|
+
typeFilter: 'all',
|
|
49
|
+
statusFilter: 'all',
|
|
50
|
+
});
|
|
51
|
+
const { loading, error } = vm.async.load;
|
|
52
|
+
// console.log('vm.paged', vm.paged)
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<h1 className="page-title">Work Sites</h1>
|
|
56
|
+
|
|
57
|
+
<SiteFilters
|
|
58
|
+
search={state.search}
|
|
59
|
+
typeFilter={state.typeFilter}
|
|
60
|
+
statusFilter={state.statusFilter}
|
|
61
|
+
onSearchChange={v => vm.setSearch(v)}
|
|
62
|
+
onTypeFilterChange={vm.setTypeFilter}
|
|
63
|
+
onStatusFilterChange={v => vm.setStatusFilter(v)}
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
<div className="results-bar">
|
|
67
|
+
<span>Showing {vm.filteredCount} of {vm.total}</span>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{vm.selection.hasSelection && (
|
|
71
|
+
<div className="selection-bar">
|
|
72
|
+
<span>{vm.selection.count} selected</span>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<DataTable
|
|
77
|
+
items={vm.paged}
|
|
78
|
+
columns={columns}
|
|
79
|
+
loading={loading}
|
|
80
|
+
error={error}
|
|
81
|
+
sort={vm.sorting}
|
|
82
|
+
selection={vm.selection}
|
|
83
|
+
pagination={vm.pagination}
|
|
84
|
+
paginationTotal={vm.filteredCount}
|
|
85
|
+
renderLoading={() => <Spinner />}
|
|
86
|
+
renderError={msg => <ErrorBanner message={msg} />}
|
|
87
|
+
renderEmpty={() => <div className="empty-state">No sites match your filters.</div>}
|
|
88
|
+
renderSortIndicator={({ active, direction }) => (
|
|
89
|
+
<span>{active ? (direction === 'asc' ? ' ↑' : ' ↓') : ''}</span>
|
|
90
|
+
)}
|
|
91
|
+
renderPagination={info => (
|
|
92
|
+
<div className="pagination-bar">
|
|
93
|
+
<button disabled={!info.hasPrev} onClick={info.goPrev}>Prev</button>
|
|
94
|
+
<span>Page {info.page} of {info.pageCount}</span>
|
|
95
|
+
<button disabled={!info.hasNext} onClick={info.goNext}>Next</button>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
className="table-container"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const __MVC_KIT_DEV__: boolean;
|
|
2
|
+
|
|
3
|
+
declare module 'react-dom/client' {
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
interface Root {
|
|
6
|
+
render(children: ReactNode): void;
|
|
7
|
+
unmount(): void;
|
|
8
|
+
}
|
|
9
|
+
export function createRoot(container: Element): Root;
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface MockWSConfig {
|
|
2
|
+
minInterval: number;
|
|
3
|
+
maxInterval: number;
|
|
4
|
+
generator: () => any;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class MockWebSocket {
|
|
8
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
9
|
+
private closed = false;
|
|
10
|
+
|
|
11
|
+
constructor(private config: MockWSConfig) {}
|
|
12
|
+
|
|
13
|
+
connect(onMessage: (data: any) => void, signal?: AbortSignal): void {
|
|
14
|
+
if (signal?.aborted) return;
|
|
15
|
+
signal?.addEventListener('abort', () => this.close(), { once: true });
|
|
16
|
+
this.scheduleNext(onMessage);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
close(): void {
|
|
20
|
+
if (this.closed) return;
|
|
21
|
+
this.closed = true;
|
|
22
|
+
if (this.timer !== null) {
|
|
23
|
+
clearTimeout(this.timer);
|
|
24
|
+
this.timer = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private scheduleNext(onMessage: (data: any) => void): void {
|
|
29
|
+
if (this.closed) return;
|
|
30
|
+
const { minInterval, maxInterval, generator } = this.config;
|
|
31
|
+
const interval = minInterval + Math.random() * (maxInterval - minInterval);
|
|
32
|
+
this.timer = setTimeout(() => {
|
|
33
|
+
if (this.closed) return;
|
|
34
|
+
onMessage(generator());
|
|
35
|
+
this.scheduleNext(onMessage);
|
|
36
|
+
}, interval);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
if (signal?.aborted) {
|
|
4
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const timer = setTimeout(resolve, ms);
|
|
9
|
+
|
|
10
|
+
signal?.addEventListener('abort', () => {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
13
|
+
}, { once: true });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function mockFetch<T>(data: T, ms?: number, signal?: AbortSignal): Promise<T> {
|
|
18
|
+
const jitter = 200 + Math.random() * 300;
|
|
19
|
+
await delay(ms ?? jitter, signal);
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** ~30% failure rate to simulate unreliable network */
|
|
24
|
+
export async function mockFetchUnreliable<T>(data: T, ms?: number, signal?: AbortSignal): Promise<T> {
|
|
25
|
+
const jitter = 200 + Math.random() * 400;
|
|
26
|
+
await delay(ms ?? jitter, signal);
|
|
27
|
+
if (Math.random() < 0.3) {
|
|
28
|
+
throw new Error('Network error: request failed');
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { MessageState } from '../types/message';
|
|
2
|
+
import type { ConversationState } from '../types/conversation';
|
|
3
|
+
import type { FeedPage } from 'mvc-kit';
|
|
4
|
+
import { mockFetch, mockFetchUnreliable } from './delay';
|
|
5
|
+
|
|
6
|
+
const PAGE_SIZE = 15;
|
|
7
|
+
|
|
8
|
+
// --- Seed conversations ---
|
|
9
|
+
const MOCK_CONVERSATIONS: ConversationState[] = [
|
|
10
|
+
{ id: 'conv1', participantIds: ['w1', 'w2'], lastMessage: 'On my way to the site', unreadCount: 2, updatedAt: Date.now() - 60000 },
|
|
11
|
+
{ id: 'conv2', participantIds: ['w1', 'w3', 'w4'], lastMessage: 'Need more supplies at Riverside', unreadCount: 0, updatedAt: Date.now() - 120000 },
|
|
12
|
+
{ id: 'conv3', participantIds: ['w1', 'w5'], lastMessage: 'Break time?', unreadCount: 1, updatedAt: Date.now() - 300000 },
|
|
13
|
+
{ id: 'conv4', participantIds: ['w1', 'w6'], lastMessage: 'Wiring specs updated', unreadCount: 0, updatedAt: Date.now() - 600000 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// --- Seed messages ---
|
|
17
|
+
const allMessages = new Map<string, MessageState[]>();
|
|
18
|
+
|
|
19
|
+
function seedMessages(convId: string, participants: string[]): MessageState[] {
|
|
20
|
+
const texts = [
|
|
21
|
+
'Hey, are you at the site yet?',
|
|
22
|
+
'Just got here. Parking is a mess.',
|
|
23
|
+
'I\'ll be there in 10 minutes.',
|
|
24
|
+
'Can you check the electrical panel on floor 3?',
|
|
25
|
+
'Done. Everything looks good.',
|
|
26
|
+
'Great work today!',
|
|
27
|
+
'Need more 2x4s for the framing.',
|
|
28
|
+
'I\'ll call the supplier.',
|
|
29
|
+
'Safety inspection is at 2pm.',
|
|
30
|
+
'Got it, I\'ll make sure the crew knows.',
|
|
31
|
+
'Weather looks bad tomorrow.',
|
|
32
|
+
'We should cover the exposed sections.',
|
|
33
|
+
'Lunch run — want anything?',
|
|
34
|
+
'Just a coffee, thanks.',
|
|
35
|
+
'The foreman wants a progress report by EOD.',
|
|
36
|
+
'On it. Almost done with section B.',
|
|
37
|
+
'New blueprints came in.',
|
|
38
|
+
'I\'ll review them tonight.',
|
|
39
|
+
'Nice, the pour came out clean.',
|
|
40
|
+
'Couldn\'t have done it without the crew.',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const messages: MessageState[] = [];
|
|
44
|
+
const count = 20 + Math.floor(Math.random() * 10);
|
|
45
|
+
const baseTime = Date.now() - count * 60000;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < count; i++) {
|
|
48
|
+
messages.push({
|
|
49
|
+
id: `${convId}-msg${i}`,
|
|
50
|
+
conversationId: convId,
|
|
51
|
+
senderId: participants[i % participants.length]!,
|
|
52
|
+
text: texts[i % texts.length]!,
|
|
53
|
+
sentAt: baseTime + i * 60000,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return messages;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const conv of MOCK_CONVERSATIONS) {
|
|
60
|
+
allMessages.set(conv.id, seedMessages(conv.id, conv.participantIds));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- API ---
|
|
64
|
+
|
|
65
|
+
export async function fetchConversations(workerId: string, signal?: AbortSignal): Promise<ConversationState[]> {
|
|
66
|
+
const convs = MOCK_CONVERSATIONS.filter(c => c.participantIds.includes(workerId));
|
|
67
|
+
return mockFetch(convs, undefined, signal);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function fetchMessages(
|
|
71
|
+
conversationId: string,
|
|
72
|
+
signal?: AbortSignal,
|
|
73
|
+
opts?: { cursor?: string | null },
|
|
74
|
+
): Promise<FeedPage<MessageState>> {
|
|
75
|
+
const messages = allMessages.get(conversationId) ?? [];
|
|
76
|
+
// Sort newest first for cursor-based pagination
|
|
77
|
+
const sorted = [...messages].sort((a, b) => b.sentAt - a.sentAt);
|
|
78
|
+
|
|
79
|
+
let startIndex = 0;
|
|
80
|
+
if (opts?.cursor) {
|
|
81
|
+
const cursorIdx = sorted.findIndex(m => m.id === opts.cursor);
|
|
82
|
+
if (cursorIdx >= 0) startIndex = cursorIdx + 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const page = sorted.slice(startIndex, startIndex + PAGE_SIZE);
|
|
86
|
+
const hasMore = startIndex + PAGE_SIZE < sorted.length;
|
|
87
|
+
const cursor = page.length > 0 ? page[page.length - 1]!.id : null;
|
|
88
|
+
|
|
89
|
+
return mockFetch({ items: page.reverse(), hasMore, cursor }, undefined, signal);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function sendMessage(
|
|
93
|
+
conversationId: string,
|
|
94
|
+
senderId: string,
|
|
95
|
+
text: string,
|
|
96
|
+
signal?: AbortSignal,
|
|
97
|
+
): Promise<MessageState> {
|
|
98
|
+
const message: MessageState = {
|
|
99
|
+
id: `${conversationId}-msg${Date.now()}`,
|
|
100
|
+
conversationId,
|
|
101
|
+
senderId,
|
|
102
|
+
text,
|
|
103
|
+
sentAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Store in mock database
|
|
107
|
+
const existing = allMessages.get(conversationId) ?? [];
|
|
108
|
+
existing.push(message);
|
|
109
|
+
allMessages.set(conversationId, existing);
|
|
110
|
+
|
|
111
|
+
// Update conversation
|
|
112
|
+
const conv = MOCK_CONVERSATIONS.find(c => c.id === conversationId);
|
|
113
|
+
if (conv) {
|
|
114
|
+
conv.lastMessage = text;
|
|
115
|
+
conv.updatedAt = Date.now();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Unreliable — ~30% chance of failure to showcase Pending retries
|
|
119
|
+
return mockFetchUnreliable(message, undefined, signal);
|
|
120
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ShiftState } from '../types/shift';
|
|
2
|
+
import { mockFetchUnreliable } from './delay';
|
|
3
|
+
|
|
4
|
+
let shiftStore: ShiftState | null = null;
|
|
5
|
+
|
|
6
|
+
export async function fetchCurrentShift(workerId: string, signal?: AbortSignal): Promise<ShiftState | null> {
|
|
7
|
+
return mockFetchUnreliable(shiftStore, undefined, signal);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function clockIn(workerId: string, siteId: string, signal?: AbortSignal): Promise<ShiftState> {
|
|
11
|
+
const shift: ShiftState = {
|
|
12
|
+
id: `shift-${Date.now()}`,
|
|
13
|
+
workerId,
|
|
14
|
+
siteId,
|
|
15
|
+
clockIn: Date.now(),
|
|
16
|
+
clockOut: null,
|
|
17
|
+
breaks: [],
|
|
18
|
+
};
|
|
19
|
+
shiftStore = shift;
|
|
20
|
+
return mockFetchUnreliable(shift, undefined, signal);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function clockOut(shiftId: string, signal?: AbortSignal): Promise<ShiftState> {
|
|
24
|
+
if (!shiftStore || shiftStore.id !== shiftId) {
|
|
25
|
+
throw new Error('No active shift');
|
|
26
|
+
}
|
|
27
|
+
// End any open break
|
|
28
|
+
const breaks = shiftStore.breaks.map(b =>
|
|
29
|
+
b.end === null ? { ...b, end: Date.now() } : b,
|
|
30
|
+
);
|
|
31
|
+
shiftStore = { ...shiftStore, clockOut: Date.now(), breaks };
|
|
32
|
+
const result = shiftStore;
|
|
33
|
+
shiftStore = null;
|
|
34
|
+
return mockFetchUnreliable(result, undefined, signal);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function startBreak(shiftId: string, signal?: AbortSignal): Promise<ShiftState> {
|
|
38
|
+
if (!shiftStore || shiftStore.id !== shiftId) {
|
|
39
|
+
throw new Error('No active shift');
|
|
40
|
+
}
|
|
41
|
+
shiftStore = {
|
|
42
|
+
...shiftStore,
|
|
43
|
+
breaks: [...shiftStore.breaks, { start: Date.now(), end: null }],
|
|
44
|
+
};
|
|
45
|
+
return mockFetchUnreliable(shiftStore, undefined, signal);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function endBreak(shiftId: string, signal?: AbortSignal): Promise<ShiftState> {
|
|
49
|
+
if (!shiftStore || shiftStore.id !== shiftId) {
|
|
50
|
+
throw new Error('No active shift');
|
|
51
|
+
}
|
|
52
|
+
const breaks = shiftStore.breaks.map(b =>
|
|
53
|
+
b.end === null ? { ...b, end: Date.now() } : b,
|
|
54
|
+
);
|
|
55
|
+
shiftStore = { ...shiftStore, breaks };
|
|
56
|
+
return mockFetchUnreliable(shiftStore, undefined, signal);
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SiteState } from '../types/site';
|
|
2
|
+
|
|
3
|
+
export const MOCK_SITES: SiteState[] = [
|
|
4
|
+
{ id: 's1', name: 'Riverside Apartments', type: 'residential', status: 'active', address: '120 River St', workerCount: 8 },
|
|
5
|
+
{ id: 's2', name: 'Downtown Office Tower', type: 'commercial', status: 'active', address: '500 Main Ave', workerCount: 15 },
|
|
6
|
+
{ id: 's3', name: 'Harbor Industrial Park', type: 'industrial', status: 'active', address: '80 Harbor Rd', workerCount: 12 },
|
|
7
|
+
{ id: 's4', name: 'Maple Ridge Homes', type: 'residential', status: 'active', address: '45 Maple Ln', workerCount: 6 },
|
|
8
|
+
{ id: 's5', name: 'City Bridge Repair', type: 'infrastructure', status: 'paused', address: 'Bridge St', workerCount: 0 },
|
|
9
|
+
{ id: 's6', name: 'Tech Campus Phase 2', type: 'commercial', status: 'active', address: '200 Innovation Dr', workerCount: 20 },
|
|
10
|
+
{ id: 's7', name: 'Waterfront Condos', type: 'residential', status: 'completed', address: '10 Bay Walk', workerCount: 0 },
|
|
11
|
+
{ id: 's8', name: 'Factory Retrofit', type: 'industrial', status: 'active', address: '300 Industrial Blvd', workerCount: 9 },
|
|
12
|
+
{ id: 's9', name: 'Highway Extension', type: 'infrastructure', status: 'active', address: 'Route 9 North', workerCount: 25 },
|
|
13
|
+
{ id: 's10', name: 'Community Center', type: 'commercial', status: 'paused', address: '15 Park Ave', workerCount: 0 },
|
|
14
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorkerState } from '../types/worker';
|
|
2
|
+
|
|
3
|
+
export const MOCK_WORKERS: WorkerState[] = [
|
|
4
|
+
{ id: 'w1', name: 'Marcus Johnson', email: 'marcus@example.com', role: 'foreman', status: 'available', avatar: 'MJ' },
|
|
5
|
+
{ id: 'w2', name: 'Sarah Chen', email: 'sarah@example.com', role: 'electrician', status: 'available', avatar: 'SC' },
|
|
6
|
+
{ id: 'w3', name: 'David Kowalski', email: 'david@example.com', role: 'plumber', status: 'on-shift', avatar: 'DK' },
|
|
7
|
+
{ id: 'w4', name: 'Ana Rodriguez', email: 'ana@example.com', role: 'carpenter', status: 'on-shift', avatar: 'AR' },
|
|
8
|
+
{ id: 'w5', name: 'James O\'Brien', email: 'james@example.com', role: 'laborer', status: 'on-break', avatar: 'JO' },
|
|
9
|
+
{ id: 'w6', name: 'Priya Patel', email: 'priya@example.com', role: 'electrician', status: 'offline', avatar: 'PP' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const CURRENT_WORKER = MOCK_WORKERS[0]!;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Model } from 'mvc-kit';
|
|
2
|
+
import type { ValidationErrors } from 'mvc-kit';
|
|
3
|
+
|
|
4
|
+
export interface ComposeMessageState {
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ComposeMessageModel extends Model<ComposeMessageState> {
|
|
9
|
+
setText(text: string) { this.set({ text }); }
|
|
10
|
+
|
|
11
|
+
protected validate(state: ComposeMessageState): ValidationErrors<ComposeMessageState> {
|
|
12
|
+
const errors: Partial<Record<keyof ComposeMessageState, string>> = {};
|
|
13
|
+
if (!state.text.trim()) errors.text = 'Message cannot be empty';
|
|
14
|
+
if (state.text.length > 500) errors.text = 'Message must be under 500 characters';
|
|
15
|
+
return errors;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Resource } from 'mvc-kit';
|
|
2
|
+
import type { ConversationState } from '../types/conversation';
|
|
3
|
+
import { fetchConversations } from '../mock/messages';
|
|
4
|
+
|
|
5
|
+
export class ConversationsResource extends Resource<ConversationState> {
|
|
6
|
+
async loadAll(workerId: string) {
|
|
7
|
+
const data = await fetchConversations(workerId, this.disposeSignal);
|
|
8
|
+
this.reset(data);
|
|
9
|
+
}
|
|
10
|
+
}
|