From 974d3fe11baa5d7a34a8eac9c9d2ee270dd58ee3 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 21 Dec 2025 09:59:17 -0500 Subject: [PATCH] Initial commit --- .gitignore | 29 + README.md | 118 ++ composer.json | 26 + composer.lock | 23 + lib/Controllers/CollectionController.php | 178 +++ lib/Controllers/EntityController.php | 202 ++++ lib/Controllers/ProviderController.php | 50 + lib/Controllers/ServiceController.php | 95 ++ lib/Manager.php | 649 ++++++++++ lib/Module.php | 65 + package-lock.json | 1384 ++++++++++++++++++++++ package.json | 23 + src/main.ts | 18 + src/models/collection.ts | 158 +++ src/models/entity.ts | 162 +++ src/models/event-location-physical.ts | 82 ++ src/models/event-location-virtual.ts | 83 ++ src/models/event-mutation.ts | 481 ++++++++ src/models/event-notification.ts | 110 ++ src/models/event-occurrence.ts | 362 ++++++ src/models/event-organizer.ts | 67 ++ src/models/event-participant.ts | 144 +++ src/models/event.ts | 574 +++++++++ src/models/index.ts | 11 + src/models/journal.ts | 157 +++ src/models/provider.ts | 63 + src/models/service.ts | 77 ++ src/models/task.ts | 204 ++++ src/services/collectionService.ts | 88 ++ src/services/entityService.ts | 100 ++ src/services/index.ts | 16 + src/services/providerService.ts | 36 + src/services/serviceService.ts | 52 + src/stores/collectionsStore.ts | 202 ++++ src/stores/entitiesStore.ts | 281 +++++ src/stores/index.ts | 8 + src/stores/providersStore.ts | 62 + src/stores/servicesStore.ts | 95 ++ src/types/collection.ts | 158 +++ src/types/common.ts | 65 + src/types/entity.ts | 166 +++ src/types/event.ts | 146 +++ src/types/index.ts | 12 + src/types/journal.ts | 33 + src/types/provider.ts | 53 + src/types/service.ts | 214 ++++ src/types/task.ts | 60 + src/utils/key-generator.ts | 31 + src/vite-env.d.ts | 1 + tsconfig.app.json | 19 + tsconfig.json | 7 + tsconfig.node.json | 25 + vite.config.ts | 30 + 53 files changed, 7555 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 lib/Controllers/CollectionController.php create mode 100644 lib/Controllers/EntityController.php create mode 100644 lib/Controllers/ProviderController.php create mode 100644 lib/Controllers/ServiceController.php create mode 100644 lib/Manager.php create mode 100644 lib/Module.php create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/main.ts create mode 100644 src/models/collection.ts create mode 100644 src/models/entity.ts create mode 100644 src/models/event-location-physical.ts create mode 100644 src/models/event-location-virtual.ts create mode 100644 src/models/event-mutation.ts create mode 100644 src/models/event-notification.ts create mode 100644 src/models/event-occurrence.ts create mode 100644 src/models/event-organizer.ts create mode 100644 src/models/event-participant.ts create mode 100644 src/models/event.ts create mode 100644 src/models/index.ts create mode 100644 src/models/journal.ts create mode 100644 src/models/provider.ts create mode 100644 src/models/service.ts create mode 100644 src/models/task.ts create mode 100644 src/services/collectionService.ts create mode 100644 src/services/entityService.ts create mode 100644 src/services/index.ts create mode 100644 src/services/providerService.ts create mode 100644 src/services/serviceService.ts create mode 100644 src/stores/collectionsStore.ts create mode 100644 src/stores/entitiesStore.ts create mode 100644 src/stores/index.ts create mode 100644 src/stores/providersStore.ts create mode 100644 src/stores/servicesStore.ts create mode 100644 src/types/collection.ts create mode 100644 src/types/common.ts create mode 100644 src/types/entity.ts create mode 100644 src/types/event.ts create mode 100644 src/types/index.ts create mode 100644 src/types/journal.ts create mode 100644 src/types/provider.ts create mode 100644 src/types/service.ts create mode 100644 src/types/task.ts create mode 100644 src/utils/key-generator.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..812e4cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Frontend development +node_modules/ +*.local +.env.local +.env.*.local +.cache/ +.vite/ +.temp/ +.tmp/ + +# Frontend build +/static/ + +# Backend development +/lib/vendor/ +coverage/ +phpunit.xml.cache +.phpunit.result.cache +.php-cs-fixer.cache +.phpstan.cache +.phpactor/ + +# Editors +.DS_Store +.vscode/ +.idea/ + +# Logs +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..0af2581 --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# Chrono Manager Module + +The Chrono Manager module provides the backend management layer for calendar, event, task, and journal functionality within the Ktrix platform. + +## Overview + +This module implements the Provider-Service-Collection-Entity architecture for chronological data: + +- **Providers**: Calendar/task service providers (local, CalDAV, Exchange, etc.) +- **Services**: Individual calendar/task services within a provider +- **Collections**: Calendars, task lists, or journal collections +- **Entities**: Individual events, tasks, or journal entries + +## Features + +- Multi-provider support for calendars and tasks +- CRUD operations for collections and entities +- Event management with recurrence support +- Task management with priorities and subtasks +- Journal entry support +- Delta synchronization +- Filtering and sorting capabilities + +## Architecture + +### PHP Backend (`lib/`) +- `Manager.php`: Core manager class handling all operations +- `Module.php`: Module registration and lifecycle + +### TypeScript Frontend (`src/`) +- **Types**: TypeScript interfaces for all data structures +- **Services**: API communication layer +- **Stores**: Pinia stores for state management +- **Models**: Data model classes with validation + +## Usage + +### Backend (PHP) + +```php +use KTXM\ChronoManager\Manager; + +// Register providers +Manager::registerProvider('local', LocalProvider::class); + +// Create manager instance +$manager = new Manager($logger); + +// List collections +$collections = $manager->collectionList($tenantId, $userId); + +// Create entity +$entity = $manager->entityCreate($tenantId, $userId, $providerId, $serviceId, $collectionId, $entityData); +``` + +### Frontend (TypeScript) + +```typescript +import { useCollectionsStore, useEntitiesStore } from '@ChronoManager/stores'; + +// Get stores +const collectionsStore = useCollectionsStore(); +const entitiesStore = useEntitiesStore(); + +// Load collections +const collections = await collectionsStore.list(); + +// Create entity +const entity = await entitiesStore.create(collection, entityData); +``` + +## Entity Types + +### Event +Calendar events with start/end times, locations, attendees, and recurrence patterns. + +### Task +Todo items with priorities, due dates, completion tracking, and subtasks. + +### Journal +Journal entries with timestamps and content. + +## Development + +### Build TypeScript + +```bash +npm install +npm run build +``` + +### Watch Mode + +```bash +npm run watch +``` + +## API Endpoints + +All endpoints are prefixed with `/m/chrono_manager/`: + +- `/provider/list` - List available providers +- `/provider/extant` - Check provider availability +- `/service/list` - List available services +- `/service/fetch` - Get service details +- `/collection/list` - List collections +- `/collection/create` - Create collection +- `/collection/modify` - Update collection +- `/collection/destroy` - Delete collection +- `/entity/list` - List entities +- `/entity/create` - Create entity +- `/entity/modify` - Update entity +- `/entity/destroy` - Delete entity +- `/entity/delta` - Get delta changes + +## License + +AGPL-3.0-or-later diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..490fa58 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "ktxm/chrono-manager", + "type": "project", + "authors": [ + { + "name": "Sebastian Krupinski", + "email": "krupinski01@gmail.com" + } + ], + "config": { + "optimize-autoloader": true, + "platform": { + "php": "8.2" + }, + "autoloader-suffix": "ChronoManager", + "vendor-dir": "lib/vendor" + }, + "require": { + "php": ">=8.2 <=8.5" + }, + "autoload": { + "psr-4": { + "KTXM\\ChronoManager\\": "lib/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..f95ba02 --- /dev/null +++ b/composer.lock @@ -0,0 +1,23 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d301e50c47a83d5bf3a8a07ddeb43301", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2 <=8.5" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.2" + }, + "plugin-api-version": "2.3.0" +} diff --git a/lib/Controllers/CollectionController.php b/lib/Controllers/CollectionController.php new file mode 100644 index 0000000..5d0b223 --- /dev/null +++ b/lib/Controllers/CollectionController.php @@ -0,0 +1,178 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ChronoManager\Controllers; + +use KTXC\Http\Response\JsonResponse; +use KTXC\SessionIdentity; +use KTXC\SessionTenant; +use KTXF\Controller\ControllerAbstract; +use KTXF\Chrono\Collection\ICollectionBase; +use KTXF\Routing\Attributes\AuthenticatedRoute; +use InvalidArgumentException; +use KTXF\Resource\Selector\SourceSelector; +use KTXM\ChronoManager\Manager; + +class CollectionController extends ControllerAbstract { + + public function __construct( + private readonly SessionTenant $tenantIdentity, + private readonly SessionIdentity $userIdentity, + private Manager $chronoManager, + ) {} + + /** + * Retrieve all collections matching criteria + * + * @param SourceSelector|null $sources collection sources + * @param array|null $filter collection filter options + * @param array|null $sort collection sorting options + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/collection/list', name: 'chronomanager.collection.list', methods: ['POST'])] + + public function list(?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null, ?string $uid = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve collections + $responseData = $this->chronoManager->collectionList($tenantId, $userId, $sources, $filter, $sort); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + + /** + * Confirm if specific collections are available for a specific user + * + * @param SourceSelector $sources collection sources + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/collection/extant', name: 'chronomanager.collection.extant', methods: ['POST'])] + + public function extant(SourceSelector $sources, ?string $uid = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve collection status + $responseData = $this->chronoManager->collectionExtant($tenantId, $userId, $sources); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + + /** + * Fetch a collection + * + * @param string $provider provider identifier + * @param string $service service identifier + * @param string|int $identifier collection identifier + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/collection/fetch', name: 'chronomanager.collection.fetch', methods: ['POST'])] + + public function fetch(string $provider, string $service, string|int $identifier, ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve collection + $responseData = $this->chronoManager->collectionFetch($tenantId, $userId, $provider, $service, $identifier); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + + /** + * Create a collection + * + * @param string $provider provider identifier + * @param string $service service identifier + * @param ICollectionBase|array $data collection to create + * @param array $options additional options + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/collection/create', name: 'chronomanager.collection.create', methods: ['POST'])] + + public function create(string $provider, string $service, ICollectionBase|array $data, array $options = [], ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // create collection + $responseData = $this->chronoManager->collectionCreate($tenantId, $userId, $provider, $service, $data, $options); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + + /** + * Modify an existing collection + * + * @param string $provider provider identifier + * @param string $service service identifier + * @param string|int $identifier collection identifier + * @param ICollectionBase|array $data collection data + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/collection/modify', name: 'chronomanager.collection.modify', methods: ['POST'])] + + public function modify(string $provider, string $service, string|int $identifier, ICollectionBase|array $data, ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // modify collection + $responseData = $this->chronoManager->collectionModify($tenantId, $userId, $provider, $service, $identifier, $data); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + + /** + * Delete a collection + * + * @param string $provider provider identifier + * @param string $service service identifier + * @param string|int $identifier collection identifier + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/collection/destroy', name: 'chronomanager.collection.destroy', methods: ['POST'])] + + public function destroy(string $provider, string $service, string|int $identifier, string|null $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // destroy collection + $success = $this->chronoManager->collectionDestroy($tenantId, $userId, $provider, $service, $identifier); + + return new JsonResponse(['success' => $success], $success ? JsonResponse::HTTP_OK : JsonResponse::HTTP_NOT_FOUND); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + +} diff --git a/lib/Controllers/EntityController.php b/lib/Controllers/EntityController.php new file mode 100644 index 0000000..b8847e7 --- /dev/null +++ b/lib/Controllers/EntityController.php @@ -0,0 +1,202 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ChronoManager\Controllers; + +use KTXC\Http\Response\JsonResponse; +use KTXC\SessionIdentity; +use KTXC\SessionTenant; +use KTXF\Controller\ControllerAbstract; +use KTXF\Routing\Attributes\AuthenticatedRoute; +use InvalidArgumentException; +use KTXF\Resource\Selector\SourceSelector; +use KTXM\ChronoManager\Manager; + +class EntityController extends ControllerAbstract { + + public function __construct( + private readonly SessionTenant $tenantIdentity, + private readonly SessionIdentity $userIdentity, + private Manager $chronoManager, + ) {} + + /** + * List entities for a specific user + * + * @param SourceSelector|null $sources entity sources + * @param array|null $filter entity filter + * @param array|null $sort entity sort + * @param array|null $range entity range + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/entity/list', name: 'chronomanager.entity.list', methods: ['POST'])] + + public function list(?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null, ?array $range = null, ?string $uid = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve entities + $responseData = $this->chronoManager->entityList($tenantId, $userId, $sources, $filter, $sort, $range); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + + /** + * Delta of entity changes since last request + * + * @param SourceSelector $sources entity sources + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/entity/delta', name: 'chronomanager.entity.delta', methods: ['POST'])] + + public function delta(SourceSelector $sources, ?string $uid = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve entity delta + $responseData = $this->chronoManager->entityDelta($tenantId, $userId, $sources); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + + /** + * Confirm if specific entities are available for a specific user + * + * @param SourceSelector $sources entity sources + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/entity/extant', name: 'chronomanager.entity.extant', methods: ['POST'])] + + public function extant(SourceSelector $sources, ?string $uid = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve entity status + $responseData = $this->chronoManager->entityExtant($tenantId, $userId, $sources); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + + /** + * Fetch specific entities from a specific collection + * + * @param string|null $uid user identifier + * @param string $provider provider identifier + * @param string $service service identifier + * @param string|int $collection collection identifier + * @param array $identifiers entity identifiers + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/entity/fetch', name: 'chronomanager.entity.fetch', methods: ['POST'])] + + public function fetch(string $provider, string $service, string|int $collection, array $identifiers, ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve entities + $responseData = $this->chronoManager->entityFetch($tenantId, $userId, $provider, $service, $collection, $identifiers); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + + /** + * Create a new entity in a collection + * + * @param string $provider provider identifier + * @param string $service service identifier + * @param string|int $collection collection identifier + * @param array $data entity to create + * @param array $options additional options + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/entity/create', name: 'chronomanager.entity.create', methods: ['POST'])] + + public function create(string $provider, string $service, string|int $collection, array $data, array $options = [], ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // create entity + $responseData = $this->chronoManager->entityCreate($tenantId, $userId, $provider, $service, $collection, $data, $options); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + + /** + * Modify an existing entity in a collection + * + * @param string $provider provider identifier + * @param string $service service identifier + * @param string|int $collection collection identifier + * @param string|int $identifier entity identifier + * @param array $entity entity with modifications + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/entity/modify', name: 'chronomanager.entity.modify', methods: ['POST'])] + + public function modify(string $provider, string $service, string|int $collection, string|int $identifier, array $data, ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // modify entity + $responseData = $this->chronoManager->entityModify($tenantId, $userId, $provider, $service, $collection, $identifier, $data); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + + /** + * Delete an entity from a collection + * + * @param string $provider provider identifier + * @param string $service service identifier + * @param string|int $collection collection identifier + * @param string|int $identifier entity identifier + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/entity/destroy', name: 'chronomanager.entity.destroy', methods: ['POST'])] + public function destroy(string $provider, string $service, string|int $collection, string|int $identifier, ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // destroy entity + $success = $this->chronoManager->entityDestroy($tenantId, $userId, $provider, $service, $collection, $identifier); + + return new JsonResponse(['success' => $success], $success ? JsonResponse::HTTP_OK : JsonResponse::HTTP_NOT_FOUND); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + +} diff --git a/lib/Controllers/ProviderController.php b/lib/Controllers/ProviderController.php new file mode 100644 index 0000000..45680cc --- /dev/null +++ b/lib/Controllers/ProviderController.php @@ -0,0 +1,50 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ChronoManager\Controllers; + +use KTXC\Http\Response\JsonResponse; +use KTXF\Controller\ControllerAbstract; +use KTXF\Resource\Selector\SourceSelector; +use KTXF\Routing\Attributes\AuthenticatedRoute; +use KTXM\ChronoManager\Manager; + +class ProviderController extends ControllerAbstract { + + public function __construct( + private Manager $chronoManager, + ) {} + + /** + * Retrieve list of available providers + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/provider/list', name: 'chronomanager.provider.list', methods: ['GET'])] + + public function list(): JsonResponse { + $providers = $this->chronoManager->providerList(); + return new JsonResponse($providers, JsonResponse::HTTP_OK); + } + + /** + * Confirm which providers are available + * + * @param SourceSelector $sources provider sources + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/provider/extant', name: 'chronomanager.provider.extant', methods: ['POST'])] + + public function extant(SourceSelector $sources): JsonResponse { + $responseData = $this->chronoManager->providerExtant($sources); + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + +} diff --git a/lib/Controllers/ServiceController.php b/lib/Controllers/ServiceController.php new file mode 100644 index 0000000..c77bc0a --- /dev/null +++ b/lib/Controllers/ServiceController.php @@ -0,0 +1,95 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ChronoManager\Controllers; + +use KTXC\Http\Response\JsonResponse; +use KTXC\SessionIdentity; +use KTXC\SessionTenant; +use KTXF\Controller\ControllerAbstract; +use KTXF\Routing\Attributes\AuthenticatedRoute; +use InvalidArgumentException; +use KTXF\Resource\Selector\SourceSelector; +use KTXM\ChronoManager\Manager; + +class ServiceController extends ControllerAbstract { + + public function __construct( + private readonly SessionTenant $tenantIdentity, + private readonly SessionIdentity $userIdentity, + private Manager $chronoManager, + ) {} + + /** + * Retrieve services for a specific user + * + * @param SourceSelector|null $sources service sources + * @param array|null $filter service filter options + * @param array|null $sort service sorting options + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/service/list', name: 'chronomanager.service.list', methods: ['POST'])] + + public function list(?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null, ?string $uid = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve services + $responseData = $this->chronoManager->serviceList($tenantId, $userId, $sources, $filter, $sort); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + + /** + * Confirm if specific services are available for a specific user + * + * @param SourceSelector $sources service sources + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/service/extant', name: 'chronomanager.service.extant', methods: ['POST'])] + + public function extant(SourceSelector $sources, ?string $uid = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve services status + $responseData = $this->chronoManager->serviceExtant($tenantId, $userId, $sources); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } + + /** + * Fetch specific service for a specific user + * + * @param string $provider provider identifier + * @param string $identifier service identifier + * @param string|null $uid user identifier + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/service/fetch', name: 'chronomanager.service.fetch', methods: ['POST'])] + public function fetch(string $provider, string $identifier, ?string $uid = null): JsonResponse { + try { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + // retrieve service + $responseData = $this->chronoManager->serviceFetch($tenantId, $userId, $provider, $identifier); + + return new JsonResponse($responseData, JsonResponse::HTTP_OK); + } catch (InvalidArgumentException $e) { + return new JsonResponse(['error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); + } + } + +} diff --git a/lib/Manager.php b/lib/Manager.php new file mode 100644 index 0000000..0129180 --- /dev/null +++ b/lib/Manager.php @@ -0,0 +1,649 @@ + collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider] + */ + public function providerList(?SourceSelector $sources = null): array { + // determine filter from sources + $filter = ($sources !== null && $sources->identifiers() !== []) ? $sources->identifiers() : null; + // retrieve providers from provider manager + return $this->providerManager->providers(ProviderInterface::TYPE_CHRONO, $filter); + } + + /** + * Confirm which providers are available + * + * @param SourceSelector|null $sources collection of provider identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => true, 'provider2' => false] + */ + public function providerExtant(?SourceSelector $sources = null): array { + // determine which providers are available + $providerFilter = $sources?->identifiers() ?? []; + $providersResolved = $this->providerManager->providers(ProviderInterface::TYPE_CHRONO, $providerFilter); + $providersAvailable = array_keys($providersResolved); + $providersUnavailable = array_diff($providerFilter, $providersAvailable); + // construct response data + $responseData = array_merge( + array_fill_keys($providersAvailable, true), + array_fill_keys($providersUnavailable, false) + ); + return $responseData; + } + + /** + * Retrieve specific provider for specific user + * + * @param string $provider provider identifier + * + * @return IProviderBase + * @throws InvalidArgumentException + */ + public function providerFetch(string $provider): IProviderBase { + // retrieve provider + $providers = $this->providerList(new SourceSelector([$provider => true])); + if (!isset($providers[$provider])) { + throw new InvalidArgumentException('Provider not found: ' . $provider); + } + return $providers[$provider]; + } + + /** + * Retrieve available services for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector|null $sources list of provider and service identifiers + * + * @return array> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]] + */ + public function serviceList(string $tenantId, string $userId, ?SourceSelector $sources = null): array { + // retrieve providers + $providers = $this->providerList($sources); + // retrieve services for each provider + $responseData = []; + foreach ($providers as $provider) { + $serviceFilter = $sources[$provider->id()] instanceof ServiceSelector ? $sources[$provider->id()]->identifiers() : []; + $services = $provider->serviceList($tenantId, $userId, $serviceFilter); + $responseData[$provider->id()] = $services; + } + return $responseData; + } + + /** + * Confirm which services are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector|null $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => false], 'provider2' => ['service2' => true, 'service3' => true]] + */ + public function serviceExtant(string $tenantId, string $userId, ?SourceSelector $sources = null): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + + // retrieve services for each available provider + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $serviceAvailability = $provider->serviceExtant($tenantId, $userId, ...$serviceSelector->identifiers()); + $responseData[$provider->id()] = $serviceAvailability; + } + return $responseData; + } + + /** + * Retrieve service for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $provider provider identifier + * @param string|int $service service identifier + * + * @return IServiceBase + * @throws InvalidArgumentException + */ + public function serviceFetch(string $tenantId, string $userId, string $provider, string|int $service): IServiceBase { + $providerInstance = $this->providerFetch($provider); + $serviceInstance = $providerInstance->serviceFetch($tenantId, $userId, $service); + if ($serviceInstance === null) { + throw new InvalidArgumentException('Service not found: ' . $service); + } + return $serviceInstance; + } + + /** + * Retrieve available collections for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector|null $sources list of provider and service identifiers + * + * @return array> collections of available services e.g. ['provider1' => ['service1' => [ICollectionBase], 'service2' => [ICollectionBase]]] + */ + public function collectionList(string $tenantId, string $userId, ?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($sources); + // retrieve services for each provider + $responseData = []; + foreach ($providers as $provider) { + $serviceFilter = $sources[$provider->id()] instanceof ServiceSelector ? $sources[$provider->id()]->identifiers() : []; + $services = $provider->serviceList($tenantId, $userId, $serviceFilter); + // retrieve collections for each service + foreach ($services as $service) { + // construct filter for collections + $collectionFilter = null; + if ($filter !== null && $filter !== []) { + $collectionFilter = $service->collectionListFilter(); + foreach ($filter as $attribute => $value) { + $collectionFilter->condition($attribute, $value); + } + } + // construct sort for collections + $collectionSort = null; + if ($sort !== null && $sort !== []) { + $collectionSort = $service->collectionListSort(); + foreach ($sort as $attribute => $direction) { + $collectionSort->condition($attribute, $direction); + } + } + $collections = $service->collectionList($collectionFilter, $collectionSort); + if ($collections !== []) { + $responseData[$provider->id()][$service->id()] = $collections; + } + } + } + return $responseData; + } + + /** + * Confirm which collections are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => ['collection1' => true, 'collection2' => false]]]] + */ + public function collectionExtant(string $tenantId, string $userId, SourceSelector $sources): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve available providers + $providers = $this->providerList($sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + + // check services and collections for each available provider + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $servicesRequested = $serviceSelector->identifiers(); + $servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested); + $servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable)); + + // mark unavailable services as false + if ($servicesUnavailable !== []) { + $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + } + + // confirm collections for each available service + foreach ($servicesAvailable as $service) { + $collectionSelector = $serviceSelector[$service->id()]; + $collectionsRequested = $collectionSelector->identifiers(); + + if ($collectionsRequested === []) { + continue; + } + + // check each requested collection + foreach ($collectionsRequested as $collectionId) { + $responseData[$provider->id()][$service->id()][$collectionId] = $service->collectionExtant((string)$collectionId); + } + } + } + return $responseData; + } + + /** + * Retrieve specific collection for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * + * @return ICollectionBase + * @throws InvalidArgumentException + */ + public function collectionFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId): ICollectionBase { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + $collection = $service->collectionFetch($collectionId); + if ($collection === null) { + throw new InvalidArgumentException('Collection not found: ' . $collectionId); + } + return $collection; + } + + /** + * Create a new collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param ICollectionBase|array $collection collection to create + * @param array $options additional options + * + * @return ICollectionBase + * @throws InvalidArgumentException + */ + public function collectionCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, ICollectionBase|array $collection, array $options = []): ICollectionBase { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + if (!($service instanceof IServiceCollectionMutable)) { + throw new InvalidArgumentException('Service does not support collection creation'); + } + // convert array to collection object if needed + if (is_array($collection)) { + $collectionObject = $service->collectionFresh(); + $collectionObject->jsonDeserialize($collection); + $collection = $collectionObject; + } + // create collection + return $service->collectionCreate('', $collection, $options); + } + + /** + * Modify an existing collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param ICollectionBase|array $collectionData collection data + * + * @return ICollectionBase + * @throws InvalidArgumentException + */ + public function collectionModify(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, ICollectionBase|array $collectionData): ICollectionBase { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + if (!($service instanceof IServiceCollectionMutable)) { + throw new InvalidArgumentException('Service does not support collection modification'); + } + // convert array to collection object if needed + if (is_array($collectionData)) { + $collectionObject = $service->collectionFresh(); + $collectionObject->jsonDeserialize($collectionData); + $collectionData = $collectionObject; + } + // modify collection + return $service->collectionModify($collectionId, $collectionData); + } + + /** + * Destroy a collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * + * @return bool + * @throws InvalidArgumentException + */ + public function collectionDestroy(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId): bool { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + if (!($service instanceof IServiceCollectionMutable)) { + throw new InvalidArgumentException('Service does not support collection destruction'); + } + return $service->collectionDestroy($collectionId); + } + + /** + * Retrieve available entities for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector $sources list of provider and service identifiers + * + * @return array> collections of store enteties e.g. ['provider1' => ['service1' => [ICollectionBase], 'service2' => [ICollectionBase]]] + */ + public function entityList(string $tenantId, string $userId, ?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null, ?array $range = null): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($sources); + // retrieve services for each provider + $responseData = []; + foreach ($providers as $provider) { + // retrieve services for each provider + $serviceSelector = $sources[$provider->id()]; + $servicesSelected = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : []; + $services = $provider->serviceList($tenantId,$userId, $servicesSelected); + foreach ($services as $service) { + // retrieve collections for each service + $collectionSelector = $serviceSelector[$service->id()]; + $collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; + if ($collectionSelected === []) { + $collections = $service->collectionList(); + $collectionSelected = array_map( + fn(ICollectionBase $collection): string|int => $collection->id(), + $collections + ); + } + if ($collectionSelected === []) { + continue; + } + // construct filter for entities + $entityFilter = null; + if ($filter !== null && $filter !== []) { + $entityFilter = $service->entityListFilter(); + foreach ($filter as $attribute => $value) { + $entityFilter->condition($attribute, $value); + } + } + // construct sort for entities + $entitySort = null; + if ($sort !== null && $sort !== []) { + $entitySort = $service->entityListSort(); + foreach ($sort as $attribute => $direction) { + $entitySort->condition($attribute, $direction); + } + } + // construct range for entities + $entityRange = null; + if ($range !== null && $range !== [] && isset($range['type'])) { + $entityRange = $service->entityListRange(RangeType::from($range['type'])); + // Cast to IRangeTally if the range type is TALLY + if ($entityRange->type() === RangeType::TALLY) { + /** @var IRangeTally $entityRange */ + if (isset($range['anchor'])) { + $entityRange->setAnchor(RangeAnchorType::from($range['anchor'])); + } + if (isset($range['position'])) { + $entityRange->setPosition($range['position']); + } + if (isset($range['tally'])) { + $entityRange->setTally($range['tally']); + } + } + } + // retrieve entities for each collection + foreach ($collectionSelected as $collectionId) { + $entities = $service->entityList($collectionId, $entityFilter, $entitySort, $entityRange, null); + // skip collections with no entities + if ($entities === []) { + continue; + } + $responseData[$provider->id()][$service->id()][$collectionId] = $entities; + } + } + } + return $responseData; + } + + /** + * Confirm which entities are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param array $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status + */ + public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + // iterate through available providers + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $servicesRequested = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : []; + $services = $provider->serviceList($tenantId, $userId, $servicesRequested); + $servicesUnavailable = array_diff($servicesRequested, array_keys($services)); + if ($servicesUnavailable !== []) { + $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + } + // iterate through available services + foreach ($services as $service) { + $collectionSelector = $serviceSelector[$service->id()]; + $collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; + if ($collectionsRequested === []) { + $responseData[$provider->id()][$service->id()] = false; + continue; + } + foreach ($collectionsRequested as $collection) { + $entitySelector = $collectionSelector[$collection] ?? null; + $responseData[$provider->id()][$service->id()][$collection] = $service->entityDelta($collection, $entitySelector); + } + } + } + return $responseData; + } + + /** + * Confirm which entities are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => ['collection1' => ['entity1' => true, 'entity2' => false]]]] + */ + public function entityExtant(string $tenantId, string $userId, SourceSelector $sources): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve available providers + $providers = $this->providerList($sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + + // check services, collections, and entities for each available provider + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $servicesRequested = $serviceSelector->identifiers(); + $servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested); + $servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable)); + + // mark unavailable services as false + if ($servicesUnavailable !== []) { + $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + } + + // check collections and entities for each available service + foreach ($servicesAvailable as $service) { + $collectionSelector = $serviceSelector[$service->id()]; + $collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; + + if ($collectionsRequested === []) { + continue; + } + + // check entities for each requested collection + foreach ($collectionsRequested as $collectionId) { + // first check if collection exists + $collectionExists = $service->collectionExtant((string)$collectionId); + + if (!$collectionExists) { + $responseData[$provider->id()][$service->id()][$collectionId] = false; + continue; + } + + // extract entity identifiers from collection selector + $entitySelector = $collectionSelector[$collectionId]; + + // handle both array of entity IDs and boolean true (meaning check if collection exists) + if ($entitySelector instanceof EntitySelector) { + $entityIds = $entitySelector->identifiers(); + foreach ($entityIds as $entityId) { + $responseData[$provider->id()][$service->id()][$collectionId][$entityId] = $service->entityExtant((string)$collectionId, (string)$entityId)[$entityId] ?? false; + } + } + } + } + } + return $responseData; + } + + /** + * Retrieve specific entities from a collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string|int $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param array $identifiers entity identifiers + * + * @return array collection of entities + */ + public function entityFetch(string $tenantId, string $userId, string|int $providerId, string|int $serviceId, string|int $collectionId, array $identifiers): array { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + return $service->entityFetch($collectionId, ...$identifiers); + } + + /** + * Create a new entity in a collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param IEntityBase|array $entity entity to create + * @param array $options additional options + * + * @return IEntityBase + * @throws InvalidArgumentException + */ + public function entityCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, IEntityBase|array $entity, array $options = []): IEntityBase { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + if (!($service instanceof IServiceEntityMutable)) { + throw new InvalidArgumentException('Service does not support entity creation'); + } + // convert array to entity object if needed + if (is_array($entity)) { + $entityObject = $service->entityFresh(); + $entityObject->jsonDeserialize($entity); + $entity = $entityObject; + } + // create entity + return $service->entityCreate($collectionId, $entity, $options); + } + + /** + * Modify an existing entity in a collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string|int $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param string|int $identifier entity identifier + * @param IEntityBase|array $entity entity with modifications + * + * @return IEntityBase + * @throws InvalidArgumentException + */ + public function entityModify(string $tenantId, string $userId, string|int $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier, IEntityBase|array $entity): IEntityBase { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + if (!($service instanceof IServiceEntityMutable)) { + throw new InvalidArgumentException('Service does not support entity modification'); + } + // convert array to entity object if needed + if (is_array($entity)) { + $entityObject = $service->entityFresh(); + $entityObject->jsonDeserialize($entity); + $entity = $entityObject; + } + // modify entity + return $service->entityModify($collectionId, $identifier, $entity); + } + + /** + * Destroy an entity from a collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string|int $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param string|int $identifier entity identifier + * + * @return bool + * @throws InvalidArgumentException + */ + public function entityDestroy(string $tenantId, string $userId, string|int $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier): bool { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + if (!($service instanceof IServiceEntityMutable)) { + throw new InvalidArgumentException('Service does not support entity destruction'); + } + $entity = $service->entityDestroy($collectionId, $identifier); + return $entity !== null; + } +} diff --git a/lib/Module.php b/lib/Module.php new file mode 100644 index 0000000..ff4bf40 --- /dev/null +++ b/lib/Module.php @@ -0,0 +1,65 @@ + [ + 'label' => 'Access Chrono Manager', + 'description' => 'View and access the chrono manager module', + 'group' => 'Calendar Management' + ], + ]; + } + + public function registerBI(): array { + return [ + 'handle' => $this->handle(), + 'namespace' => 'ChronoManager', + 'version' => $this->version(), + 'label' => $this->label(), + 'author' => $this->author(), + 'description' => $this->description(), + 'boot' => 'static/module.mjs', + ]; + } + +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..68374fa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1384 @@ +{ + "name": "@ktxm/chrono-manager-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ktxm/chrono-manager-ui", + "version": "1.0.0", + "dependencies": { + "pinia": "^2.3.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.10.1", + "@vue/tsconfig": "^0.8.0", + "typescript": "~5.7.2", + "vite": "^6.0.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tsconfig/node22": { + "version": "22.0.5", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.5.tgz", + "integrity": "sha512-hLf2ld+sYN/BtOJjHUWOk568dvjFQkHnLNa6zce25GIH+vxKfvTgm3qpaH6ToF5tu/NN0IH66s+Bb5wElHrLcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz", + "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.24", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz", + "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz", + "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.24", + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz", + "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", + "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", + "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", + "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.24", + "@vue/runtime-core": "3.5.24", + "@vue/shared": "3.5.24", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", + "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24" + }, + "peerDependencies": { + "vue": "3.5.24" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz", + "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz", + "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-sfc": "3.5.24", + "@vue/runtime-dom": "3.5.24", + "@vue/server-renderer": "3.5.24", + "@vue/shared": "3.5.24" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f6de6a8 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@ktxm/chrono-manager-ui", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build --mode production --config vite.config.ts", + "dev": "vite build --mode development --config vite.config.ts", + "watch": "vite build --mode development --watch --config vite.config.ts", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "pinia": "^2.3.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.10.1", + "@vue/tsconfig": "^0.8.0", + "typescript": "~5.7.2", + "vite": "^6.0.3" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..7bebac0 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,18 @@ +import { useCollectionsStore } from '@/stores/collectionsStore' +import { useEntitiesStore } from '@/stores/entitiesStore' +import { useProvidersStore } from '@/stores/providersStore' +import { useServicesStore } from '@/stores/servicesStore' + +/** + * Chrono Manager Module Boot Script + * + * This script is executed when the chrono_manager module is loaded. + * It initializes the chronoStore which manages calendars, events, tasks, and journals state. + */ + +console.log('[ChronoManager] Booting Chrono Manager module...') + +console.log('[ChronoManager] Chrono Manager module booted successfully') + +// Export store for external use if needed +export { useCollectionsStore, useEntitiesStore, useProvidersStore, useServicesStore } diff --git a/src/models/collection.ts b/src/models/collection.ts new file mode 100644 index 0000000..454e631 --- /dev/null +++ b/src/models/collection.ts @@ -0,0 +1,158 @@ +/** + * Class model for Collection Interface + */ +import type { + CollectionInterface, + CollectionContentsInterface, + CollectionPermissionsInterface, +} from "@/types/collection"; + +export class CollectionObject implements CollectionInterface { + + _data!: CollectionInterface; + + constructor() { + this._data = { + '@type': 'chrono:collection', + provider: null, + service: null, + in: null, + id: null, + label: null, + description: null, + priority: null, + visibility: null, + color: null, + enabled: true, + signature: null, + permissions: {}, + contents: {}, + }; + } + + fromJson(data: CollectionInterface): CollectionObject { + this._data = data; + return this; + } + + toJson(): CollectionInterface { + return this._data; + } + + clone(): CollectionObject { + const cloned = new CollectionObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get provider(): string | null { + return this._data.provider; + } + + set provider(value: string | null) { + this._data.provider = value; + } + + get service(): string | null { + return this._data.service; + } + + set service(value: string | null) { + this._data.service = value; + } + + get in(): number | string | null { + return this._data.in; + } + + set in(value: number | string | null) { + this._data.in = value; + } + + get id(): number | string | null { + return this._data.id; + } + + set id(value: number | string | null) { + this._data.id = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get description(): string | null { + return this._data.description; + } + + set description(value: string | null) { + this._data.description = value; + } + + get priority(): number | null { + return this._data.priority; + } + + set priority(value: number | null) { + this._data.priority = value; + } + + get visibility(): string | null { + return this._data.visibility; + } + + set visibility(value: string | null) { + this._data.visibility = value; + } + + get color(): string | null { + return this._data.color; + } + + set color(value: string | null) { + this._data.color = value; + } + + get enabled(): boolean { + return this._data.enabled; + } + + set enabled(value: boolean) { + this._data.enabled = value; + } + + get signature(): string | null { + return this._data.signature; + } + + set signature(value: string | null) { + this._data.signature = value; + } + + get permissions(): CollectionPermissionsInterface { + return this._data.permissions; + } + + set permissions(value: CollectionPermissionsInterface) { + this._data.permissions = value; + } + + get contents(): CollectionContentsInterface { + return this._data.contents; + } + + set contents(value: CollectionContentsInterface) { + this._data.contents = value; + } + +} diff --git a/src/models/entity.ts b/src/models/entity.ts new file mode 100644 index 0000000..fa85075 --- /dev/null +++ b/src/models/entity.ts @@ -0,0 +1,162 @@ +/** + * Class model for Entity Interface + */ +import type { EntityInterface } from "@/types/entity"; +import type { EventInterface } from "@/types/event"; +import type { TaskInterface } from "@/types/task"; +import type { JournalInterface } from "@/types/journal"; +import { EventObject } from "./event"; +import { TaskObject } from "./task"; +import { JournalObject } from "./journal"; + +export class EntityObject implements EntityInterface { + + _data!: EntityInterface; + + constructor() { + this._data = { + '@type': 'chrono:entity', + version: 1, + in: null, + id: null, + createdOn: null, + createdBy: null, + modifiedOn: null, + modifiedBy: null, + signature: null, + data: null, + }; + } + + fromJson(data: EntityInterface): EntityObject { + this._data = data; + if (data.data) { + const type = data.data.type; + if (type === 'task') { + this._data.data = new TaskObject().fromJson(data.data as TaskInterface); + } else if (type === 'journal') { + this._data.data = new JournalObject().fromJson(data.data as JournalInterface); + } else { + this._data.data = new EventObject().fromJson(data.data as EventInterface); + } + } else { + this._data.data = null; + } + return this; + } + + toJson(): EntityInterface { + const json = { ...this._data }; + if (this._data.data instanceof EventObject || + this._data.data instanceof TaskObject || + this._data.data instanceof JournalObject) { + json.data = this._data.data.toJson(); + } + return json; + } + + clone(): EntityObject { + const cloned = new EntityObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get version(): number { + return this._data.version; + } + + set version(value: number) { + this._data.version = value; + } + + get in(): string | number | null { + return this._data.in; + } + + set in(value: string | number | null) { + this._data.in = value; + } + + get id(): string | number | null { + return this._data.id; + } + + set id(value: string | number | null) { + this._data.id = value; + } + + get createdOn(): Date | null { + return this._data.createdOn; + } + + set createdOn(value: Date | null) { + this._data.createdOn = value; + } + + get createdBy(): string | null { + return this._data.createdBy; + } + + set createdBy(value: string | null) { + this._data.createdBy = value; + } + + get modifiedOn(): Date | null { + return this._data.modifiedOn; + } + + set modifiedOn(value: Date | null) { + this._data.modifiedOn = value; + } + + get modifiedBy(): string | null { + return this._data.modifiedBy; + } + + set modifiedBy(value: string | null) { + this._data.modifiedBy = value; + } + + get signature(): string | null { + return this._data.signature; + } + + set signature(value: string | null) { + this._data.signature = value; + } + + get data(): EventObject | TaskObject | JournalObject | null { + if (this._data.data instanceof EventObject || + this._data.data instanceof TaskObject || + this._data.data instanceof JournalObject) { + return this._data.data; + } + + if (this._data.data) { + const type = this._data.data.type; + let hydrated; + if (type === 'task') { + hydrated = new TaskObject().fromJson(this._data.data as TaskInterface); + } else if (type === 'journal') { + hydrated = new JournalObject().fromJson(this._data.data as JournalInterface); + } else { + hydrated = new EventObject().fromJson(this._data.data as EventInterface); + } + this._data.data = hydrated; + return hydrated; + } + + return null; + } + + set data(value: EventObject | TaskObject | JournalObject | null) { + this._data.data = value; + } + +} diff --git a/src/models/event-location-physical.ts b/src/models/event-location-physical.ts new file mode 100644 index 0000000..2486408 --- /dev/null +++ b/src/models/event-location-physical.ts @@ -0,0 +1,82 @@ +/** + * Class model for Event Physical Location Interface + */ +import type { EventLocationPhysicalInterface } from "@/types/event"; + +export class EventLocationPhysicalObject implements EventLocationPhysicalInterface { + + _data!: EventLocationPhysicalInterface; + + constructor() { + this._data = { + identifier: null + }; + } + + fromJson(data: EventLocationPhysicalInterface): EventLocationPhysicalObject { + this._data = data; + return this; + } + + toJson(): EventLocationPhysicalInterface { + const result: Partial = {}; + + for (const key in this._data) { + const value = this._data[key as keyof EventLocationPhysicalInterface]; + if (value !== null && value !== undefined) { + (result as any)[key] = value; + } + } + + return result as EventLocationPhysicalInterface; + } + + clone(): EventLocationPhysicalObject { + const cloned = new EventLocationPhysicalObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get identifier(): string | null { + return this._data.identifier; + } + + set identifier(value: string | null) { + this._data.identifier = value; + } + + get label(): string | null { + return this._data.label ?? null; + } + + set label(value: string | null) { + this._data.label = value; + } + + get description(): string | null { + return this._data.description ?? null; + } + + set description(value: string | null) { + this._data.description = value; + } + + get relation(): 'start' | 'end' | null { + return this._data.relation ?? null; + } + + set relation(value: 'start' | 'end' | null) { + this._data.relation = value; + } + + get timeZone(): string | null { + return this._data.timeZone ?? null; + } + + set timeZone(value: string | null) { + this._data.timeZone = value; + } + +} diff --git a/src/models/event-location-virtual.ts b/src/models/event-location-virtual.ts new file mode 100644 index 0000000..0f89a65 --- /dev/null +++ b/src/models/event-location-virtual.ts @@ -0,0 +1,83 @@ +/** + * Class model for Event Virtual Location Interface + */ +import type { EventLocationVirtualInterface } from "@/types/event"; + +export class EventLocationVirtualObject implements EventLocationVirtualInterface { + + _data!: EventLocationVirtualInterface; + + constructor() { + this._data = { + identifier: null, + location: '' + }; + } + + fromJson(data: EventLocationVirtualInterface): EventLocationVirtualObject { + this._data = data; + return this; + } + + toJson(): EventLocationVirtualInterface { + const result: Partial = {}; + + for (const key in this._data) { + const value = this._data[key as keyof EventLocationVirtualInterface]; + if (value !== null && value !== undefined) { + (result as any)[key] = value; + } + } + + return result as EventLocationVirtualInterface; + } + + clone(): EventLocationVirtualObject { + const cloned = new EventLocationVirtualObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get identifier(): string | null { + return this._data.identifier; + } + + set identifier(value: string | null) { + this._data.identifier = value; + } + + get label(): string | null { + return this._data.label ?? null; + } + + set label(value: string | null) { + this._data.label = value; + } + + get description(): string | null { + return this._data.description ?? null; + } + + set description(value: string | null) { + this._data.description = value; + } + + get relation(): string | null { + return this._data.relation ?? null; + } + + set relation(value: string | null) { + this._data.relation = value; + } + + get location(): string { + return this._data.location; + } + + set location(value: string) { + this._data.location = value; + } + +} diff --git a/src/models/event-mutation.ts b/src/models/event-mutation.ts new file mode 100644 index 0000000..bdf4b9b --- /dev/null +++ b/src/models/event-mutation.ts @@ -0,0 +1,481 @@ +/** + * Class model for Event Mutation Interface + */ +import type { + EventMutation, + EventLocationPhysicalInterface, + EventLocationVirtualInterface, + EventOrganizerInterface, + EventParticipantInterface, + EventNotificationInterface +} from "@/types/event"; +import { EventLocationPhysicalObject } from "./event-location-physical"; +import { EventLocationVirtualObject } from "./event-location-virtual"; +import { EventOrganizerObject } from "./event-organizer"; +import { EventParticipantObject } from "./event-participant"; +import { EventNotificationObject } from "./event-notification"; +import { generateKey } from "../utils/key-generator"; + +export class EventMutationObject implements EventMutation { + + _data!: EventMutation; + + constructor() { + this._data = { + mutationId: null, + mutationTz: null, + mutationExclusion: null, + sequence: null, + timeZone: null, + startsOn: null, + startsTZ: null, + endsOn: null, + endsTZ: null, + duration: null, + timeless: null, + label: null, + description: null, + locationsPhysical: {}, + locationsVirtual: {}, + availability: null, + priority: null, + sensitivity: null, + color: null, + tags: [], + organizer: { + realm: 'E', + address: '' + }, + participants: {}, + notifications: {} + }; + } + + fromJson(data: EventMutation): EventMutationObject { + this._data = JSON.parse(JSON.stringify(data)); + + // Convert organizer to object instance + if (this._data.organizer) { + this._data.organizer = new EventOrganizerObject().fromJson(this._data.organizer) as any; + } + + // Convert locations physical to object instances + for (const key in this._data.locationsPhysical) { + const loc = this._data.locationsPhysical[key]; + if (loc) { + this._data.locationsPhysical[key] = new EventLocationPhysicalObject().fromJson(loc) as any; + } + } + + // Convert locations virtual to object instances + for (const key in this._data.locationsVirtual) { + const loc = this._data.locationsVirtual[key]; + if (loc) { + this._data.locationsVirtual[key] = new EventLocationVirtualObject().fromJson(loc) as any; + } + } + + // Convert participants to object instances + for (const key in this._data.participants) { + const participant = this._data.participants[key]; + if (participant) { + this._data.participants[key] = new EventParticipantObject().fromJson(participant) as any; + } + } + + // Convert notifications to object instances + for (const key in this._data.notifications) { + const notification = this._data.notifications[key]; + if (notification) { + this._data.notifications[key] = new EventNotificationObject().fromJson(notification) as any; + } + } + + return this; + } + + toJson(): EventMutation { + return JSON.parse(JSON.stringify(this._data)); + } + + clone(): EventMutationObject { + const cloned = new EventMutationObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get mutationId(): string | null { + return this._data.mutationId; + } + + set mutationId(value: string | null) { + this._data.mutationId = value; + } + + get mutationTz(): string | null { + return this._data.mutationTz; + } + + set mutationTz(value: string | null) { + this._data.mutationTz = value; + } + + get mutationExclusion(): boolean | null { + return this._data.mutationExclusion; + } + + set mutationExclusion(value: boolean | null) { + this._data.mutationExclusion = value; + } + + get sequence(): number | null { + return this._data.sequence; + } + + set sequence(value: number | null) { + this._data.sequence = value; + } + + get timeZone(): string | null { + return this._data.timeZone; + } + + set timeZone(value: string | null) { + this._data.timeZone = value; + } + + get startsOn(): string | null { + return this._data.startsOn; + } + + set startsOn(value: string | null) { + this._data.startsOn = value; + } + + get startsTZ(): string | null { + return this._data.startsTZ; + } + + set startsTZ(value: string | null) { + this._data.startsTZ = value; + } + + get endsOn(): string | null { + return this._data.endsOn; + } + + set endsOn(value: string | null) { + this._data.endsOn = value; + } + + get endsTZ(): string | null { + return this._data.endsTZ; + } + + set endsTZ(value: string | null) { + this._data.endsTZ = value; + } + + get duration(): string | null { + return this._data.duration; + } + + set duration(value: string | null) { + this._data.duration = value; + } + + get timeless(): boolean | null { + return this._data.timeless; + } + + set timeless(value: boolean | null) { + this._data.timeless = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get description(): string | null { + return this._data.description; + } + + set description(value: string | null) { + this._data.description = value; + } + + get locationsPhysical(): Record { + return this._data.locationsPhysical as any; + } + + set locationsPhysical(value: Record) { + this._data.locationsPhysical = {} as any; + for (const key in value) { + const loc = value[key]; + if (loc) { + if (loc instanceof EventLocationPhysicalObject) { + (this._data.locationsPhysical as any)[key] = loc; + } else { + (this._data.locationsPhysical as any)[key] = new EventLocationPhysicalObject().fromJson(loc); + } + } + } + } + + addLocationPhysical(value?: EventLocationPhysicalInterface|EventLocationPhysicalObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let location: EventLocationPhysicalObject; + + if (!value) { + location = new EventLocationPhysicalObject(); + location.identifier = key; + } + else if (value instanceof EventLocationPhysicalObject) { + location = value; + location.identifier = key; + } + else { + location = new EventLocationPhysicalObject().fromJson(value); + location.identifier = key; + } + + (this._data.locationsPhysical as any)[key] = location; + + return key; + } + + removeLocationPhysical(key: string): void { + delete this._data.locationsPhysical[key]; + } + + getLocationPhysical(key: string): EventLocationPhysicalObject | undefined { + return this._data.locationsPhysical[key] as any; + } + + get locationsVirtual(): Record { + return this._data.locationsVirtual as any; + } + + set locationsVirtual(value: Record) { + this._data.locationsVirtual = {} as any; + for (const key in value) { + const loc = value[key]; + if (loc) { + if (loc instanceof EventLocationVirtualObject) { + (this._data.locationsVirtual as any)[key] = loc; + } else { + (this._data.locationsVirtual as any)[key] = new EventLocationVirtualObject().fromJson(loc); + } + } + } + } + + addLocationVirtual(value?: EventLocationVirtualInterface|EventLocationVirtualObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let location: EventLocationVirtualObject; + + if (!value) { + location = new EventLocationVirtualObject(); + location.identifier = key; + } + else if (value instanceof EventLocationVirtualObject) { + location = value; + location.identifier = key; + } + else { + location = new EventLocationVirtualObject().fromJson(value); + location.identifier = key; + } + + (this._data.locationsVirtual as any)[key] = location; + + return key; + } + + removeLocationVirtual(key: string): void { + delete this._data.locationsVirtual[key]; + } + + getLocationVirtual(key: string): EventLocationVirtualObject | undefined { + return this._data.locationsVirtual[key] as any; + } + + get availability(): 'free' | 'busy' | null { + return this._data.availability; + } + + set availability(value: 'free' | 'busy' | null) { + this._data.availability = value; + } + + get priority(): number | null { + return this._data.priority; + } + + set priority(value: number | null) { + this._data.priority = value; + } + + get sensitivity(): 'public' | 'private' | 'secret' | null { + return this._data.sensitivity; + } + + set sensitivity(value: 'public' | 'private' | 'secret' | null) { + this._data.sensitivity = value; + } + + get color(): string | null { + return this._data.color; + } + + set color(value: string | null) { + this._data.color = value; + } + + get tags(): string[] { + return this._data.tags; + } + + set tags(value: string[]) { + this._data.tags = value; + } + + addTag(tag: string): void { + if (!this._data.tags.includes(tag)) { + this._data.tags.push(tag); + } + } + + removeTag(tag: string): void { + const index = this._data.tags.indexOf(tag); + if (index > -1) { + this._data.tags.splice(index, 1); + } + } + + get organizer(): EventOrganizerObject | null { + return this._data.organizer as any; + } + + set organizer(value: EventOrganizerInterface|EventOrganizerObject|null) { + if (!value) { + this._data.organizer = null; + } else if (value instanceof EventOrganizerObject) { + this._data.organizer = value as any; + } else { + this._data.organizer = new EventOrganizerObject().fromJson(value) as any; + } + } + + get participants(): Record { + return this._data.participants as any; + } + + set participants(value: Record) { + this._data.participants = {} as any; + for (const key in value) { + const participant = value[key]; + if (participant) { + if (participant instanceof EventParticipantObject) { + (this._data.participants as any)[key] = participant; + } else { + (this._data.participants as any)[key] = new EventParticipantObject().fromJson(participant); + } + } + } + } + + addParticipant(value?: EventParticipantInterface|EventParticipantObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let participant: EventParticipantObject; + + if (!value) { + participant = new EventParticipantObject(); + participant.identifier = key; + } + else if (value instanceof EventParticipantObject) { + participant = value; + participant.identifier = key; + } + else { + participant = new EventParticipantObject().fromJson(value); + participant.identifier = key; + } + + (this._data.participants as any)[key] = participant; + + return key; + } + + removeParticipant(key: string): void { + delete this._data.participants[key]; + } + + getParticipant(key: string): EventParticipantObject | undefined { + return this._data.participants[key] as any; + } + + get notifications(): Record { + return this._data.notifications as any; + } + + set notifications(value: Record) { + this._data.notifications = {} as any; + for (const key in value) { + const notification = value[key]; + if (notification) { + if (notification instanceof EventNotificationObject) { + (this._data.notifications as any)[key] = notification; + } else { + (this._data.notifications as any)[key] = new EventNotificationObject().fromJson(notification); + } + } + } + } + + addNotification(value?: EventNotificationInterface|EventNotificationObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let notification: EventNotificationObject; + + if (!value) { + notification = new EventNotificationObject(); + } + else if (value instanceof EventNotificationObject) { + notification = value; + } + else { + notification = new EventNotificationObject().fromJson(value); + } + + (this._data.notifications as any)[key] = notification; + + return key; + } + + removeNotification(key: string): void { + delete this._data.notifications[key]; + } + + getNotification(key: string): EventNotificationObject | undefined { + return this._data.notifications[key] as any; + } + +} diff --git a/src/models/event-notification.ts b/src/models/event-notification.ts new file mode 100644 index 0000000..519b459 --- /dev/null +++ b/src/models/event-notification.ts @@ -0,0 +1,110 @@ +/** + * Class model for Event Notification Interface + */ +import type { EventNotificationInterface } from "@/types/event"; + +export class EventNotificationObject implements EventNotificationInterface { + + _data!: EventNotificationInterface; + + constructor() { + this._data = { + identifier: null, + type: 'email', + pattern: 'unknown' + }; + } + + fromJson(data: EventNotificationInterface): EventNotificationObject { + this._data = data; + return this; + } + + toJson(): EventNotificationInterface { + const result: Partial = {}; + + for (const key in this._data) { + const value = this._data[key as keyof EventNotificationInterface]; + if (value !== null && value !== undefined) { + (result as any)[key] = value; + } + } + + return result as EventNotificationInterface; + } + + clone(): EventNotificationObject { + const cloned = new EventNotificationObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get identifier(): string | null { + return this._data.identifier; + } + + set identifier(value: string | null) { + this._data.identifier = value; + } + + get type(): 'visual' | 'audible' | 'email' { + return this._data.type; + } + + set type(value: 'visual' | 'audible' | 'email') { + this._data.type = value; + } + + get pattern(): 'absolute' | 'relative' | 'unknown' { + return this._data.pattern; + } + + set pattern(value: 'absolute' | 'relative' | 'unknown') { + this._data.pattern = value; + if (value === 'absolute') { + this._data.anchor = null; + this._data.offset = null; + } else if (value === 'relative') { + this._data.when = null; + } else { + this._data.when = null; + this._data.anchor = null; + this._data.offset = null; + } + } + + get when(): string | null { + return this._data.when ?? null; + } + + set when(value: string | null) { + this._data.pattern = 'absolute'; + this._data.anchor = null; + this._data.offset = null; + this._data.when = value; + } + + get anchor(): 'start' | 'end' | null { + return this._data.anchor ?? null; + } + + set anchor(value: 'start' | 'end' | null) { + this._data.pattern = 'relative'; + this._data.when = null; + this._data.anchor = value; + } + + get offset(): string | null { + return this._data.offset ?? null; + } + + set offset(value: string | null) { + this._data.pattern = 'relative'; + this._data.anchor = 'start'; + this._data.when = null; + this._data.offset = value; + } + +} diff --git a/src/models/event-occurrence.ts b/src/models/event-occurrence.ts new file mode 100644 index 0000000..18d2794 --- /dev/null +++ b/src/models/event-occurrence.ts @@ -0,0 +1,362 @@ +/** + * Class model for Event Occurrence Interface + */ +import type { EventOccurrence } from "@/types/event"; + +export class EventOccurrenceObject implements EventOccurrence { + + _data!: EventOccurrence; + + constructor() { + this._data = { + pattern: 'absolute', + precision: 'daily', + interval: 1, + }; + } + + fromJson(data: EventOccurrence): EventOccurrenceObject { + this._data = data; + return this; + } + + toJson(): EventOccurrence { + const result: Partial = {}; + + for (const key in this._data) { + const value = this._data[key as keyof EventOccurrence]; + if (value !== null && value !== undefined) { + (result as any)[key] = value; + } + } + + return result as EventOccurrence; + } + + clone(): EventOccurrenceObject { + const cloned = new EventOccurrenceObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get pattern(): 'absolute' | 'relative' { + return this._data.pattern; + } + + set pattern(value: 'absolute' | 'relative') { + this._data.pattern = value; + } + + get precision(): 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly' { + return this._data.precision; + } + + set precision(value: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly') { + this._data.precision = value; + } + + get interval(): number { + return this._data.interval; + } + + set interval(value: number) { + this._data.interval = value; + } + + get iterations(): number | null { + return this._data.iterations ?? null; + } + + set iterations(value: number | null) { + this._data.iterations = value; + } + + get concludes(): string | null { + return this._data.concludes ?? null; + } + + set concludes(value: string | null) { + this._data.concludes = value; + } + + get scale(): string | null { + return this._data.scale ?? null; + } + + set scale(value: string | null) { + this._data.scale = value; + } + + get onDayOfWeek(): number[] | null { + return this._data.onDayOfWeek ?? null; + } + + set onDayOfWeek(value: number[] | null) { + this._data.onDayOfWeek = value; + } + + addOnDayOfWeek(value: number): void { + if (!this._data.onDayOfWeek) { + this._data.onDayOfWeek = []; + } + if (!this._data.onDayOfWeek.includes(value)) { + this._data.onDayOfWeek.push(value); + } + } + + removeOnDayOfWeek(value: number): void { + if (!this._data.onDayOfWeek) { + return; + } + const index = this._data.onDayOfWeek.indexOf(value); + if (index > -1) { + this._data.onDayOfWeek.splice(index, 1); + } + } + + get onDayOfMonth(): number[] | null { + return this._data.onDayOfMonth ?? null; + } + + set onDayOfMonth(value: number[] | null) { + this._data.onDayOfMonth = value; + } + + addOnDayOfMonth(value: number): void { + if (!this._data.onDayOfMonth) { + this._data.onDayOfMonth = []; + } + if (!this._data.onDayOfMonth.includes(value)) { + this._data.onDayOfMonth.push(value); + } + } + + removeOnDayOfMonth(value: number): void { + if (!this._data.onDayOfMonth) { + return; + } + const index = this._data.onDayOfMonth.indexOf(value); + if (index > -1) { + this._data.onDayOfMonth.splice(index, 1); + } + } + + get onDayOfYear(): number[] | null { + return this._data.onDayOfYear ?? null; + } + + set onDayOfYear(value: number[] | null) { + this._data.onDayOfYear = value; + } + + addOnDayOfYear(value: number): void { + if (!this._data.onDayOfYear) { + this._data.onDayOfYear = []; + } + if (!this._data.onDayOfYear.includes(value)) { + this._data.onDayOfYear.push(value); + } + } + + removeOnDayOfYear(value: number): void { + if (!this._data.onDayOfYear) { + return; + } + const index = this._data.onDayOfYear.indexOf(value); + if (index > -1) { + this._data.onDayOfYear.splice(index, 1); + } + } + + get onWeekOfMonth(): number[] | null { + return this._data.onWeekOfMonth ?? null; + } + + set onWeekOfMonth(value: number[] | null) { + this._data.onWeekOfMonth = value; + } + + addOnWeekOfMonth(value: number): void { + if (!this._data.onWeekOfMonth) { + this._data.onWeekOfMonth = []; + } + if (!this._data.onWeekOfMonth.includes(value)) { + this._data.onWeekOfMonth.push(value); + } + } + + removeOnWeekOfMonth(value: number): void { + if (!this._data.onWeekOfMonth) { + return; + } + const index = this._data.onWeekOfMonth.indexOf(value); + if (index > -1) { + this._data.onWeekOfMonth.splice(index, 1); + } + } + + get onWeekOfYear(): number[] | null { + return this._data.onWeekOfYear ?? null; + } + + set onWeekOfYear(value: number[] | null) { + this._data.onWeekOfYear = value; + } + + addOnWeekOfYear(value: number): void { + if (!this._data.onWeekOfYear) { + this._data.onWeekOfYear = []; + } + if (!this._data.onWeekOfYear.includes(value)) { + this._data.onWeekOfYear.push(value); + } + } + + removeOnWeekOfYear(value: number): void { + if (!this._data.onWeekOfYear) { + return; + } + const index = this._data.onWeekOfYear.indexOf(value); + if (index > -1) { + this._data.onWeekOfYear.splice(index, 1); + } + } + + get onMonthOfYear(): number[] | null { + return this._data.onMonthOfYear ?? null; + } + + set onMonthOfYear(value: number[] | null) { + this._data.onMonthOfYear = value; + } + + addOnMonthOfYear(value: number): void { + if (!this._data.onMonthOfYear) { + this._data.onMonthOfYear = []; + } + if (!this._data.onMonthOfYear.includes(value)) { + this._data.onMonthOfYear.push(value); + } + } + + removeOnMonthOfYear(value: number): void { + if (!this._data.onMonthOfYear) { + return; + } + const index = this._data.onMonthOfYear.indexOf(value); + if (index > -1) { + this._data.onMonthOfYear.splice(index, 1); + } + } + + get onHour(): number[] | null { + return this._data.onHour ?? null; + } + + set onHour(value: number[] | null) { + this._data.onHour = value; + } + + addOnHour(value: number): void { + if (!this._data.onHour) { + this._data.onHour = []; + } + if (!this._data.onHour.includes(value)) { + this._data.onHour.push(value); + } + } + + removeOnHour(value: number): void { + if (!this._data.onHour) { + return; + } + const index = this._data.onHour.indexOf(value); + if (index > -1) { + this._data.onHour.splice(index, 1); + } + } + + get onMinute(): number[] | null { + return this._data.onMinute ?? null; + } + + set onMinute(value: number[] | null) { + this._data.onMinute = value; + } + + addOnMinute(value: number): void { + if (!this._data.onMinute) { + this._data.onMinute = []; + } + if (!this._data.onMinute.includes(value)) { + this._data.onMinute.push(value); + } + } + + removeOnMinute(value: number): void { + if (!this._data.onMinute) { + return; + } + const index = this._data.onMinute.indexOf(value); + if (index > -1) { + this._data.onMinute.splice(index, 1); + } + } + + get onSecond(): number[] | null { + return this._data.onSecond ?? null; + } + + set onSecond(value: number[] | null) { + this._data.onSecond = value; + } + + addOnSecond(value: number): void { + if (!this._data.onSecond) { + this._data.onSecond = []; + } + if (!this._data.onSecond.includes(value)) { + this._data.onSecond.push(value); + } + } + + removeOnSecond(value: number): void { + if (!this._data.onSecond) { + return; + } + const index = this._data.onSecond.indexOf(value); + if (index > -1) { + this._data.onSecond.splice(index, 1); + } + } + + get onPosition(): number[] | null { + return this._data.onPosition ?? null; + } + + set onPosition(value: number[] | null) { + this._data.onPosition = value; + } + + addOnPosition(value: number): void { + if (!this._data.onPosition) { + this._data.onPosition = []; + } + if (!this._data.onPosition.includes(value)) { + this._data.onPosition.push(value); + } + } + + removeOnPosition(value: number): void { + if (!this._data.onPosition) { + return; + } + const index = this._data.onPosition.indexOf(value); + if (index > -1) { + this._data.onPosition.splice(index, 1); + } + } + +} diff --git a/src/models/event-organizer.ts b/src/models/event-organizer.ts new file mode 100644 index 0000000..41ed86c --- /dev/null +++ b/src/models/event-organizer.ts @@ -0,0 +1,67 @@ +/** + * Class model for Event Organizer Interface + */ +import type { EventOrganizerInterface } from "@/types/event"; + +export class EventOrganizerObject implements EventOrganizerInterface { + + _data!: EventOrganizerInterface; + + constructor() { + this._data = { + realm: 'E', + address: '' + }; + } + + fromJson(data: EventOrganizerInterface): EventOrganizerObject { + this._data = data; + return this; + } + + toJson(): EventOrganizerInterface { + const result: Partial = {}; + + for (const key in this._data) { + const value = this._data[key as keyof EventOrganizerInterface]; + if (value !== null && value !== undefined) { + (result as any)[key] = value; + } + } + + return result as EventOrganizerInterface; + } + + clone(): EventOrganizerObject { + const cloned = new EventOrganizerObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get realm(): 'I' | 'E' { + return this._data.realm; + } + + set realm(value: 'I' | 'E') { + this._data.realm = value; + } + + get address(): string { + return this._data.address; + } + + set address(value: string) { + this._data.address = value; + } + + get name(): string | null { + return this._data.name ?? null; + } + + set name(value: string | null) { + this._data.name = value; + } + +} diff --git a/src/models/event-participant.ts b/src/models/event-participant.ts new file mode 100644 index 0000000..c69ad6a --- /dev/null +++ b/src/models/event-participant.ts @@ -0,0 +1,144 @@ +/** + * Class model for Event Participant Interface + */ +import type { EventParticipantInterface } from "@/types/event"; + +export class EventParticipantObject implements EventParticipantInterface { + + _data!: EventParticipantInterface; + + constructor() { + this._data = { + identifier: null, + realm: 'E', + address: '', + type: 'individual', + status: 'none', + roles: [] + }; + } + + fromJson(data: EventParticipantInterface): EventParticipantObject { + this._data = data; + return this; + } + + toJson(): EventParticipantInterface { + const result: Partial = {}; + + for (const key in this._data) { + const value = this._data[key as keyof EventParticipantInterface]; + if (value !== null && value !== undefined) { + (result as any)[key] = value; + } + } + + return result as EventParticipantInterface; + } + + clone(): EventParticipantObject { + const cloned = new EventParticipantObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get identifier(): string | null { + return this._data.identifier; + } + + set identifier(value: string | null) { + this._data.identifier = value; + } + + get realm(): 'I' | 'E' { + return this._data.realm; + } + + set realm(value: 'I' | 'E') { + this._data.realm = value; + } + + get name(): string | null { + return this._data.name ?? null; + } + + set name(value: string | null) { + this._data.name = value; + } + + get description(): string | null { + return this._data.description ?? null; + } + + set description(value: string | null) { + this._data.description = value; + } + + get language(): string | null { + return this._data.language ?? null; + } + + set language(value: string | null) { + this._data.language = value; + } + + get address(): string { + return this._data.address; + } + + set address(value: string) { + this._data.address = value; + } + + get type(): 'unknown' | 'individual' | 'group' | 'resource' | 'location' { + return this._data.type; + } + + set type(value: 'unknown' | 'individual' | 'group' | 'resource' | 'location') { + this._data.type = value; + } + + get status(): 'none' | 'accepted' | 'declined' | 'tentative' | 'delegated' { + return this._data.status; + } + + set status(value: 'none' | 'accepted' | 'declined' | 'tentative' | 'delegated') { + this._data.status = value; + } + + get comment(): string | null { + return this._data.comment ?? null; + } + + set comment(value: string | null) { + this._data.comment = value; + } + + get roles(): ('owner' | 'chair' | 'attendee' | 'optional' | 'informational' | 'contact')[] { + return this._data.roles; + } + + set roles(value: ('owner' | 'chair' | 'attendee' | 'optional' | 'informational' | 'contact')[]) { + this._data.roles = value; + } + + addRole(role: 'owner' | 'chair' | 'attendee' | 'optional' | 'informational' | 'contact'): void { + if (!this._data.roles.includes(role)) { + this._data.roles.push(role); + } + } + + removeRole(role: 'owner' | 'chair' | 'attendee' | 'optional' | 'informational' | 'contact'): void { + const index = this._data.roles.indexOf(role); + if (index > -1) { + this._data.roles.splice(index, 1); + } + } + + hasRole(role: 'owner' | 'chair' | 'attendee' | 'optional' | 'informational' | 'contact'): boolean { + return this._data.roles.includes(role); + } + +} diff --git a/src/models/event.ts b/src/models/event.ts new file mode 100644 index 0000000..5ae1187 --- /dev/null +++ b/src/models/event.ts @@ -0,0 +1,574 @@ +/** + * Class model for Event Interface + */ +import type { + EventInterface, + EventLocationPhysicalInterface, + EventLocationVirtualInterface, + EventOrganizerInterface, + EventParticipantInterface, + EventNotificationInterface, + EventOccurrence, + EventMutation +} from "@/types/event"; +import { EventLocationPhysicalObject } from "./event-location-physical"; +import { EventLocationVirtualObject } from "./event-location-virtual"; +import { EventOrganizerObject } from "./event-organizer"; +import { EventParticipantObject } from "./event-participant"; +import { EventNotificationObject } from "./event-notification"; +import { EventOccurrenceObject } from "./event-occurrence"; +import { EventMutationObject } from "./event-mutation"; +import { generateKey } from "../utils/key-generator"; + +export class EventObject implements EventInterface { + + _data!: EventInterface; + + constructor() { + this._data = { + type: 'event', + version: 1, + urid: null, + created: null, + modified: null, + sequence: null, + timeZone: null, + startsOn: null, + startsTZ: null, + endsOn: null, + endsTZ: null, + duration: null, + timeless: null, + label: null, + description: null, + locationsPhysical: {}, + locationsVirtual: {}, + availability: null, + sensitivity: null, + priority: null, + color: null, + tags: [], + organizer: null, + participants: {}, + notifications: {}, + pattern: null, + mutations: {} + }; + } + + fromJson(data: EventInterface): EventObject { + // Deep copy the data to avoid reference issues + this._data = JSON.parse(JSON.stringify(data)); + + // Convert organizer to object instance + if (this._data.organizer) { + this._data.organizer = new EventOrganizerObject().fromJson(this._data.organizer) as any; + } + + // Convert locations physical to object instances + for (const key in this._data.locationsPhysical) { + const loc = this._data.locationsPhysical[key]; + if (loc) { + this._data.locationsPhysical[key] = new EventLocationPhysicalObject().fromJson(loc) as any; + } + } + + // Convert locations virtual to object instances + for (const key in this._data.locationsVirtual) { + const loc = this._data.locationsVirtual[key]; + if (loc) { + this._data.locationsVirtual[key] = new EventLocationVirtualObject().fromJson(loc) as any; + } + } + + // Convert participants to object instances + for (const key in this._data.participants) { + const participant = this._data.participants[key]; + if (participant) { + this._data.participants[key] = new EventParticipantObject().fromJson(participant) as any; + } + } + + // Convert notifications to object instances + for (const key in this._data.notifications) { + const notification = this._data.notifications[key]; + if (notification) { + this._data.notifications[key] = new EventNotificationObject().fromJson(notification) as any; + } + } + + // Convert pattern to object instance + if (this._data.pattern) { + this._data.pattern = new EventOccurrenceObject().fromJson(this._data.pattern) as any; + } + + // Convert mutations to object instances + for (const key in this._data.mutations) { + const mutation = this._data.mutations[key]; + if (mutation) { + this._data.mutations[key] = new EventMutationObject().fromJson(mutation) as any; + } + } + + return this; + } + + toJson(): EventInterface { + // Return a deep copy to avoid external modifications + return JSON.parse(JSON.stringify(this._data)); + } + + clone(): EventObject { + const cloned = new EventObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get type(): string { + return this._data.type; + } + + get version(): number { + return this._data.version; + } + + set version(value: number) { + this._data.version = value; + } + + get urid(): string | null { + return this._data.urid; + } + + set urid(value: string | null) { + this._data.urid = value; + } + + get created(): string | null { + return this._data.created; + } + + set created(value: string | null) { + this._data.created = value; + } + + get modified(): string | null { + return this._data.modified; + } + + set modified(value: string | null) { + this._data.modified = value; + } + + get sequence(): number | null { + return this._data.sequence; + } + + set sequence(value: number | null) { + this._data.sequence = value; + } + + get timeZone(): string | null { + return this._data.timeZone; + } + + set timeZone(value: string | null) { + this._data.timeZone = value; + } + + get startsOn(): string | null { + return this._data.startsOn; + } + + set startsOn(value: string | null) { + this._data.startsOn = value; + } + + get startsTZ(): string | null { + return this._data.startsTZ; + } + + set startsTZ(value: string | null) { + this._data.startsTZ = value; + } + + get endsOn(): string | null { + return this._data.endsOn; + } + + set endsOn(value: string | null) { + this._data.endsOn = value; + } + + get endsTZ(): string | null { + return this._data.endsTZ; + } + + set endsTZ(value: string | null) { + this._data.endsTZ = value; + } + + get duration(): string | null { + return this._data.duration; + } + + set duration(value: string | null) { + this._data.duration = value; + } + + get timeless(): boolean | null { + return this._data.timeless; + } + + set timeless(value: boolean | null) { + this._data.timeless = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get description(): string | null { + return this._data.description; + } + + set description(value: string | null) { + this._data.description = value; + } + + get availability(): 'free' | 'busy' | null { + return this._data.availability; + } + + set availability(value: 'free' | 'busy' | null) { + this._data.availability = value; + } + + get sensitivity(): 'public' | 'private' | 'secret' | null { + return this._data.sensitivity; + } + + set sensitivity(value: 'public' | 'private' | 'secret' | null) { + this._data.sensitivity = value; + } + + get priority(): number | null { + return this._data.priority; + } + + set priority(value: number | null) { + this._data.priority = value; + } + + get color(): string | null { + return this._data.color; + } + + set color(value: string | null) { + this._data.color = value; + } + + get locationsPhysical(): Record { + return this._data.locationsPhysical as any; + } + + set locationsPhysical(value: Record) { + this._data.locationsPhysical = {} as any; + for (const key in value) { + const loc = value[key]; + if (loc) { + if (loc instanceof EventLocationPhysicalObject) { + (this._data.locationsPhysical as any)[key] = loc; + } else { + (this._data.locationsPhysical as any)[key] = new EventLocationPhysicalObject().fromJson(loc); + } + } + } + } + + addLocationPhysical(value?: EventLocationPhysicalInterface|EventLocationPhysicalObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let location: EventLocationPhysicalObject; + + if (!value) { + location = new EventLocationPhysicalObject(); + location.identifier = key; + } + else if (value instanceof EventLocationPhysicalObject) { + location = value; + location.identifier = key; + } + else { + location = new EventLocationPhysicalObject().fromJson(value); + location.identifier = key; + } + + (this._data.locationsPhysical as any)[key] = location; + + return key; + } + + removeLocationPhysical(key: string): void { + delete this._data.locationsPhysical[key]; + } + + getLocationPhysical(key: string): EventLocationPhysicalObject | undefined { + return this._data.locationsPhysical[key] as any; + } + + get locationsVirtual(): Record { + return this._data.locationsVirtual as any; + } + + set locationsVirtual(value: Record) { + this._data.locationsVirtual = {} as any; + for (const key in value) { + const loc = value[key]; + if (loc) { + if (loc instanceof EventLocationVirtualObject) { + (this._data.locationsVirtual as any)[key] = loc; + } else { + (this._data.locationsVirtual as any)[key] = new EventLocationVirtualObject().fromJson(loc); + } + } + } + } + + addLocationVirtual(value?: EventLocationVirtualInterface|EventLocationVirtualObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let location: EventLocationVirtualObject; + + if (!value) { + location = new EventLocationVirtualObject(); + location.identifier = key; + } + else if (value instanceof EventLocationVirtualObject) { + location = value; + location.identifier = key; + } + else { + location = new EventLocationVirtualObject().fromJson(value); + location.identifier = key; + } + + (this._data.locationsVirtual as any)[key] = location; + + return key; + } + + removeLocationVirtual(key: string): void { + delete this._data.locationsVirtual[key]; + } + + getLocationVirtual(key: string): EventLocationVirtualObject | undefined { + return this._data.locationsVirtual[key] as any; + } + + get tags(): string[] { + return this._data.tags; + } + + set tags(value: string[]) { + this._data.tags = value; + } + + addTag(tag: string): void { + if (!this._data.tags.includes(tag)) { + this._data.tags.push(tag); + } + } + + removeTag(tag: string): void { + const index = this._data.tags.indexOf(tag); + if (index > -1) { + this._data.tags.splice(index, 1); + } + } + + get organizer(): EventOrganizerObject { + return this._data.organizer as any; + } + + set organizer(value: EventOrganizerInterface|EventOrganizerObject) { + if (value instanceof EventOrganizerObject) { + (this._data.organizer as any) = value; + } else { + (this._data.organizer as any) = new EventOrganizerObject().fromJson(value); + } + } + + get participants(): Record { + return this._data.participants as any; + } + + set participants(value: Record) { + this._data.participants = {} as any; + for (const key in value) { + const participant = value[key]; + if (participant) { + if (participant instanceof EventParticipantObject) { + (this._data.participants as any)[key] = participant; + } else { + (this._data.participants as any)[key] = new EventParticipantObject().fromJson(participant); + } + } + } + } + + getParticipant(key: string): EventParticipantObject | undefined { + return this._data.participants[key] as any; + } + + addParticipant(value?: EventParticipantInterface|EventParticipantObject|null, key?: string): string { + + if (!key) { + key = generateKey(); + } + + let participant: EventParticipantObject; + + if (!value) { + participant = new EventParticipantObject(); + participant.identifier = key; + } + else if (value instanceof EventParticipantObject) { + participant = value; + participant.identifier = key; + } + else { + participant = new EventParticipantObject().fromJson(value); + participant.identifier = key; + } + + (this._data.participants as any)[key] = participant; + + return key; + } + + removeParticipant(key: string): void { + delete this._data.participants[key]; + } + + get notifications(): Record { + return this._data.notifications as any; + } + + set notifications(value: Record) { + this._data.notifications = {} as any; + for (const key in value) { + const notification = value[key]; + if (notification) { + if (notification instanceof EventNotificationObject) { + (this._data.notifications as any)[key] = notification; + } else { + (this._data.notifications as any)[key] = new EventNotificationObject().fromJson(notification); + } + } + } + } + + addNotification(value?: EventNotificationInterface|EventNotificationObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let notification: EventNotificationObject; + + if (!value) { + notification = new EventNotificationObject(); + } + else if (value instanceof EventNotificationObject) { + notification = value; + } + else { + notification = new EventNotificationObject().fromJson(value); + } + + (this._data.notifications as any)[key] = notification; + + return key; + } + + removeNotification(key: string): void { + delete this._data.notifications[key]; + } + + getNotification(key: string): EventNotificationObject | undefined { + return this._data.notifications[key] as any; + } + + get pattern(): EventOccurrenceObject | null { + return this._data.pattern as any; + } + + set pattern(value: EventOccurrence|EventOccurrenceObject|null) { + if (!value) { + this._data.pattern = null; + } else if (value instanceof EventOccurrenceObject) { + this._data.pattern = value as any; + } else { + this._data.pattern = new EventOccurrenceObject().fromJson(value) as any; + } + } + + get mutations(): Record { + return this._data.mutations as any; + } + + set mutations(value: Record) { + this._data.mutations = {} as any; + for (const key in value) { + const mutation = value[key]; + if (mutation) { + if (mutation instanceof EventMutationObject) { + (this._data.mutations as any)[key] = mutation; + } else { + (this._data.mutations as any)[key] = new EventMutationObject().fromJson(mutation); + } + } + } + } + + addMutation(value?: EventMutation|EventMutationObject|null, key?: string): string { + if (!key) { + key = generateKey(); + } + + let mutation: EventMutationObject; + + if (!value) { + mutation = new EventMutationObject(); + } + else if (value instanceof EventMutationObject) { + mutation = value; + } + else { + mutation = new EventMutationObject().fromJson(value); + } + + (this._data.mutations as any)[key] = mutation; + + return key; + } + + removeMutation(key: string): void { + delete this._data.mutations[key]; + } + + getMutation(key: string): EventMutationObject | undefined { + return this._data.mutations[key] as any; + } + +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..3d07bee --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,11 @@ +/** + * Central export point for all Chrono Manager models + */ + +export { Collection } from './collection'; +export { Entity } from './entity'; +export { Event } from './event'; +export { Task } from './task'; +export { Journal } from './journal'; +export { Provider } from './provider'; +export { Service } from './service'; diff --git a/src/models/journal.ts b/src/models/journal.ts new file mode 100644 index 0000000..e4cc5e3 --- /dev/null +++ b/src/models/journal.ts @@ -0,0 +1,157 @@ +/** + * Class model for Journal Interface + */ +import type { + JournalInterface, + JournalAttachment +} from "@/types/journal"; + +export class JournalObject implements JournalInterface { + + _data!: JournalInterface; + + constructor() { + this._data = { + type: 'journal', + version: 0, + urid: null, + created: null, + modified: null, + label: null, + description: null, + content: null, + startsOn: null, + endsOn: null, + status: null, + visibility: null, + attachments: {}, + tags: [], + }; + } + + fromJson(data: JournalInterface): JournalObject { + this._data = data; + return this; + } + + toJson(): JournalInterface { + return this._data; + } + + clone(): JournalObject { + const cloned = new JournalObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get type(): string { + return this._data.type; + } + + get version(): number { + return this._data.version; + } + + set version(value: number) { + this._data.version = value; + } + + get urid(): string | null { + return this._data.urid; + } + + set urid(value: string | null) { + this._data.urid = value; + } + + get created(): Date | null { + return this._data.created; + } + + set created(value: Date | null) { + this._data.created = value; + } + + get modified(): Date | null { + return this._data.modified; + } + + set modified(value: Date | null) { + this._data.modified = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get description(): string | null { + return this._data.description; + } + + set description(value: string | null) { + this._data.description = value; + } + + get content(): string | null { + return this._data.content; + } + + set content(value: string | null) { + this._data.content = value; + } + + get startsOn(): Date | null { + return this._data.startsOn; + } + + set startsOn(value: Date | null) { + this._data.startsOn = value; + } + + get endsOn(): Date | null { + return this._data.endsOn; + } + + set endsOn(value: Date | null) { + this._data.endsOn = value; + } + + get status(): string | null { + return this._data.status; + } + + set status(value: string | null) { + this._data.status = value; + } + + get visibility(): string | null { + return this._data.visibility; + } + + set visibility(value: string | null) { + this._data.visibility = value; + } + + get attachments(): Record { + return this._data.attachments; + } + + set attachments(value: Record) { + this._data.attachments = value; + } + + get tags(): string[] { + return this._data.tags; + } + + set tags(value: string[]) { + this._data.tags = value; + } + +} diff --git a/src/models/provider.ts b/src/models/provider.ts new file mode 100644 index 0000000..ce22be6 --- /dev/null +++ b/src/models/provider.ts @@ -0,0 +1,63 @@ +/** + * Class model for Provider Interface + */ +import type { ProviderCapabilitiesInterface, ProviderInterface } from "@/types/provider"; + +export class ProviderObject implements ProviderInterface { + + _data!: ProviderInterface; + + constructor() { + this._data = { + '@type': 'chrono:provider', + id: '', + label: '', + capabilities: {}, + }; + } + + fromJson(data: ProviderInterface): ProviderObject { + this._data = data; + return this; + } + + toJson(): ProviderInterface { + return this._data; + } + + clone(): ProviderObject { + const cloned = new ProviderObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + capable(capability: keyof ProviderCapabilitiesInterface): boolean { + return !!(this._data.capabilities && this._data.capabilities[capability]); + } + + capability(capability: keyof ProviderCapabilitiesInterface): any | null { + if (this._data.capabilities) { + return this._data.capabilities[capability]; + } + return null; + } + + /** Immutable Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get id(): string { + return this._data.id; + } + + get label(): string { + return this._data.label; + } + + get capabilities(): ProviderCapabilitiesInterface { + return this._data.capabilities; + } + +} diff --git a/src/models/service.ts b/src/models/service.ts new file mode 100644 index 0000000..7766372 --- /dev/null +++ b/src/models/service.ts @@ -0,0 +1,77 @@ +/** + * Class model for Service Interface + */ +import type { ServiceCapabilitiesInterface, ServiceInterface } from "@/types/service"; + +export class ServiceObject implements ServiceInterface { + + _data!: ServiceInterface; + + constructor() { + this._data = { + '@type': 'chrono:service', + provider: '', + id: '', + label: '', + capabilities: {}, + enabled: true, + }; + } + + fromJson(data: ServiceInterface): ServiceObject { + this._data = data; + return this; + } + + toJson(): ServiceInterface { + return this._data; + } + + clone(): ServiceObject { + const cloned = new ServiceObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + capable(capability: keyof ServiceCapabilitiesInterface): boolean { + return !!(this._data.capabilities && this._data.capabilities[capability]); + } + + capability(capability: keyof ServiceCapabilitiesInterface): any | null { + if (this._data.capabilities) { + return this._data.capabilities[capability]; + } + return null; + } + + /** Immutable Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get provider(): string { + return this._data.provider; + } + + get id(): string { + return this._data.id; + } + + get label(): string { + return this._data.label; + } + + get capabilities(): ServiceCapabilitiesInterface | undefined { + return this._data.capabilities; + } + + get enabled(): boolean { + return this._data.enabled; + } + + set enabled(value: boolean) { + this._data.enabled = value; + } + +} diff --git a/src/models/task.ts b/src/models/task.ts new file mode 100644 index 0000000..390b9c3 --- /dev/null +++ b/src/models/task.ts @@ -0,0 +1,204 @@ +/** + * Class model for Task Interface + */ +import type { + TaskInterface, + TaskSubtask, + TaskAttachment, + TaskRecurrence +} from "@/types/task"; + +export class TaskObject implements TaskInterface { + + _data!: TaskInterface; + + constructor() { + this._data = { + type: 'task', + version: 0, + urid: null, + created: null, + modified: null, + label: null, + description: null, + startsOn: null, + dueOn: null, + completedOn: null, + status: null, + priority: null, + progress: null, + color: null, + recurrence: null, + subtasks: {}, + attachments: {}, + tags: [], + notes: null, + }; + } + + fromJson(data: TaskInterface): TaskObject { + this._data = data; + return this; + } + + toJson(): TaskInterface { + return this._data; + } + + clone(): TaskObject { + const cloned = new TaskObject(); + cloned._data = JSON.parse(JSON.stringify(this._data)); + return cloned; + } + + /** Properties */ + + get type(): string { + return this._data.type; + } + + get version(): number { + return this._data.version; + } + + set version(value: number) { + this._data.version = value; + } + + get urid(): string | null { + return this._data.urid; + } + + set urid(value: string | null) { + this._data.urid = value; + } + + get created(): Date | null { + return this._data.created; + } + + set created(value: Date | null) { + this._data.created = value; + } + + get modified(): Date | null { + return this._data.modified; + } + + set modified(value: Date | null) { + this._data.modified = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get description(): string | null { + return this._data.description; + } + + set description(value: string | null) { + this._data.description = value; + } + + get startsOn(): Date | null { + return this._data.startsOn; + } + + set startsOn(value: Date | null) { + this._data.startsOn = value; + } + + get dueOn(): Date | null { + return this._data.dueOn; + } + + set dueOn(value: Date | null) { + this._data.dueOn = value; + } + + get completedOn(): Date | null { + return this._data.completedOn; + } + + set completedOn(value: Date | null) { + this._data.completedOn = value; + } + + get status(): string | null { + return this._data.status; + } + + set status(value: string | null) { + this._data.status = value; + } + + get priority(): number | null { + return this._data.priority; + } + + set priority(value: number | null) { + this._data.priority = value; + } + + get progress(): number | null { + return this._data.progress; + } + + set progress(value: number | null) { + this._data.progress = value; + } + + get color(): string | null { + return this._data.color; + } + + set color(value: string | null) { + this._data.color = value; + } + + get recurrence(): TaskRecurrence | null { + return this._data.recurrence; + } + + set recurrence(value: TaskRecurrence | null) { + this._data.recurrence = value; + } + + get subtasks(): Record { + return this._data.subtasks; + } + + set subtasks(value: Record) { + this._data.subtasks = value; + } + + get attachments(): Record { + return this._data.attachments; + } + + set attachments(value: Record) { + this._data.attachments = value; + } + + get tags(): string[] { + return this._data.tags; + } + + set tags(value: string[]) { + this._data.tags = value; + } + + get notes(): string | null { + return this._data.notes; + } + + set notes(value: string | null) { + this._data.notes = value; + } + +} diff --git a/src/services/collectionService.ts b/src/services/collectionService.ts new file mode 100644 index 0000000..294cfe6 --- /dev/null +++ b/src/services/collectionService.ts @@ -0,0 +1,88 @@ +/** + * Collection management service + */ + +import { createFetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper-core'; + +const fetchWrapper = createFetchWrapper(); +import type { + CollectionListRequest, + CollectionListResponse, + CollectionExtantRequest, + CollectionExtantResponse, + CollectionFetchRequest, + CollectionFetchResponse, + CollectionCreateRequest, + CollectionCreateResponse, + CollectionModifyRequest, + CollectionModifyResponse, + CollectionDestroyRequest, + CollectionDestroyResponse, +} from '../types/collection'; + +const BASE_URL = '/m/chrono_manager/collection'; + +export const collectionService = { + + /** + * List all available collections + * + * @param request - Collection list request parameters + * @returns Promise with collection list grouped by provider and service + */ + async list(request: CollectionListRequest = {}): Promise { + return await fetchWrapper.post(`${BASE_URL}/list`, request); + }, + + /** + * Check which collections exist/are available + * + * @param request - Collection extant request with source selector + * @returns Promise with collection availability status + */ + async extant(request: CollectionExtantRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/extant`, request); + }, + + /** + * Fetch a specific collection + * + * @param request - Collection fetch request + * @returns Promise with collection details + */ + async fetch(request: CollectionFetchRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/fetch`, request); + }, + + /** + * Create a new collection + * + * @param request - Collection create request + * @returns Promise with created collection + */ + async create(request: CollectionCreateRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/create`, request); + }, + + /** + * Modify an existing collection + * + * @param request - Collection modify request + * @returns Promise with modified collection + */ + async modify(request: CollectionModifyRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/modify`, request); + }, + + /** + * Delete a collection + * + * @param request - Collection destroy request + * @returns Promise with deletion result + */ + async destroy(request: CollectionDestroyRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/destroy`, request); + }, +}; + +export default collectionService; diff --git a/src/services/entityService.ts b/src/services/entityService.ts new file mode 100644 index 0000000..6ba2387 --- /dev/null +++ b/src/services/entityService.ts @@ -0,0 +1,100 @@ +/** + * Entity management service + */ + +import { createFetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper-core'; + +const fetchWrapper = createFetchWrapper(); +import type { + EntityListRequest, + EntityListResponse, + EntityDeltaRequest, + EntityDeltaResponse, + EntityExtantRequest, + EntityExtantResponse, + EntityFetchRequest, + EntityFetchResponse, + EntityCreateRequest, + EntityCreateResponse, + EntityModifyRequest, + EntityModifyResponse, + EntityDestroyRequest, + EntityDestroyResponse, +} from '../types/entity'; + +const BASE_URL = '/m/chrono_manager/entity'; + +export const entityService = { + + /** + * List all available entities (events, tasks, journals) + * + * @param request - Entity list request parameters + * @returns Promise with entity list grouped by provider, service, and collection + */ + async list(request: EntityListRequest = {}): Promise { + return await fetchWrapper.post(`${BASE_URL}/list`, request); + }, + + /** + * Get delta changes for entities + * + * @param request - Entity delta request with source selector + * @returns Promise with delta changes (created, modified, deleted) + */ + async delta(request: EntityDeltaRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/delta`, request); + }, + + /** + * Check which entities exist/are available + * + * @param request - Entity extant request with source selector + * @returns Promise with entity availability status + */ + async extant(request: EntityExtantRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/extant`, request); + }, + + /** + * Fetch specific entities + * + * @param request - Entity fetch request + * @returns Promise with entity details + */ + async fetch(request: EntityFetchRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/fetch`, request); + }, + + /** + * Create a new entity + * + * @param request - Entity create request + * @returns Promise with created entity + */ + async create(request: EntityCreateRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/create`, request); + }, + + /** + * Modify an existing entity + * + * @param request - Entity modify request + * @returns Promise with modified entity + */ + async modify(request: EntityModifyRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/modify`, request); + }, + + /** + * Delete an entity + * + * @param request - Entity destroy request + * @returns Promise with deletion result + */ + async destroy(request: EntityDestroyRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/destroy`, request); + }, +}; + +export default entityService; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..f713c00 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,16 @@ +/** + * Central export point for all Chrono Manager services + */ + +// Services +export { providerService } from './providerService'; +export { serviceService } from './serviceService'; +export { collectionService } from './collectionService'; +export { entityService } from './entityService'; + +// Type exports +export type * from '../types/common'; +export type * from '../types/provider'; +export type * from '../types/service'; +export type * from '../types/collection'; +export type * from '../types/entity'; diff --git a/src/services/providerService.ts b/src/services/providerService.ts new file mode 100644 index 0000000..375b0e1 --- /dev/null +++ b/src/services/providerService.ts @@ -0,0 +1,36 @@ +/** + * Provider management service + */ + +import { createFetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper-core'; + +const fetchWrapper = createFetchWrapper(); +import type { ProviderListResponse, ProviderExtantResponse } from '../types/provider'; +import type { SourceSelector } from '../types/common'; + +const BASE_URL = '/m/chrono_manager/provider'; + +export const providerService = { + + /** + * List all available providers + * + * @returns Promise with provider list keyed by provider ID + */ + async list(): Promise { + return await fetchWrapper.get(`${BASE_URL}/list`); + }, + + /** + * Check which providers exist/are available + * + * @param sources - Source selector with provider IDs to check + * + * @returns Promise with provider availability status + */ + async extant(sources: SourceSelector): Promise { + return await fetchWrapper.post(`${BASE_URL}/extant`, { sources }); + }, +}; + +export default providerService; diff --git a/src/services/serviceService.ts b/src/services/serviceService.ts new file mode 100644 index 0000000..4460e45 --- /dev/null +++ b/src/services/serviceService.ts @@ -0,0 +1,52 @@ +/** + * Service management service + */ + +import { createFetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper-core'; + +const fetchWrapper = createFetchWrapper(); +import type { + ServiceListRequest, + ServiceListResponse, + ServiceExtantRequest, + ServiceExtantResponse, + ServiceFetchRequest, + ServiceFetchResponse, +} from '../types/service'; + +const BASE_URL = '/m/chrono_manager/service'; + +export const serviceService = { + + /** + * List all available services + * + * @param request - Service list request parameters + * @returns Promise with service list grouped by provider + */ + async list(request: ServiceListRequest = {}): Promise { + return await fetchWrapper.post(`${BASE_URL}/list`, request); + }, + + /** + * Check which services exist/are available + * + * @param request - Service extant request with source selector + * @returns Promise with service availability status + */ + async extant(request: ServiceExtantRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/extant`, request); + }, + + /** + * Fetch a specific service + * + * @param request - Service fetch request with provider and service IDs + * @returns Promise with service details + */ + async fetch(request: ServiceFetchRequest): Promise { + return await fetchWrapper.post(`${BASE_URL}/fetch`, request); + }, +}; + +export default serviceService; diff --git a/src/stores/collectionsStore.ts b/src/stores/collectionsStore.ts new file mode 100644 index 0000000..25c4600 --- /dev/null +++ b/src/stores/collectionsStore.ts @@ -0,0 +1,202 @@ +/** + * Chrono Manager - Collections Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { collectionService } from '../services/collectionService'; +import type { + SourceSelector, + ListFilter, + ListSort, +} from '../types/common'; +import { CollectionObject } from '../models/collection'; +import type { ServiceObject } from '../models/service'; +import type { CollectionInterface } from '../types/collection'; + +export const useCollectionsStore = defineStore('chronoCollectionsStore', () => { + // State + const collections = ref([]); + + // Actions + + /** + * Retrieve collections from the server + */ + async function list( + sources?: SourceSelector, + filter?: ListFilter, + sort?: ListSort, + uid?: string + ): Promise { + try { + const response = await collectionService.list({ sources, filter, sort, uid }); + + // Flatten the nested response into a flat array + const flatCollections: CollectionObject[] = []; + Object.entries(response).forEach(([_providerId, providerCollections]) => { + Object.entries(providerCollections).forEach(([_serviceId, serviceCollections]) => { + Object.values(serviceCollections).forEach((collection: CollectionInterface) => { + flatCollections.push(new CollectionObject().fromJson(collection)); + }); + }); + }); + + console.debug('[Chrono Manager](Store) - Successfully retrieved', flatCollections.length, 'collections:', flatCollections.map(c => ({ + id: c.id, + label: c.label, + service: c.service, + provider: c.provider + }))); + + collections.value = flatCollections; + return flatCollections; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to retrieve collections:', error); + throw error; + } + } + + /** + * Fetch a specific collection + */ + async function fetch( + provider: string, + service: string, + identifier: string | number, + uid?: string + ): Promise { + try { + const response = await collectionService.fetch({ provider, service, identifier, uid }); + + return new CollectionObject().fromJson(response); + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to fetch collection:', error); + throw error; + } + } + + /** + * Create a fresh collection object with default values + */ + function fresh(): CollectionObject { + return new CollectionObject(); + } + + /** + * Create a new collection + */ + async function create( + service: ServiceObject, + collection: CollectionObject, + options?: string[], + uid?: string + ): Promise { + try { + if (service.provider === null || service.id === null) { + throw new Error('Invalid service object, must have a provider and identifier'); + } + + const response = await collectionService.create({ + provider: service.provider, + service: service.id, + data: collection.toJson(), + options, + uid + }); + + const createdCollection = new CollectionObject().fromJson(response); + collections.value.push(createdCollection); + + console.debug('[Chrono Manager](Store) - Successfully created collection'); + + return createdCollection; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to create collection:', error); + throw error; + } + } + + /** + * Modify an existing collection + */ + async function modify( + collection: CollectionObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await collectionService.modify({ + provider: collection.provider, + service: collection.service, + identifier: collection.id, + data: collection.toJson(), + uid + }); + + const modifiedCollection = new CollectionObject().fromJson(response); + const index = collections.value.findIndex(c => c.id === collection.id); + if (index !== -1) { + collections.value[index] = modifiedCollection; + } + + console.debug('[Chrono Manager](Store) - Successfully modified collection'); + + return modifiedCollection; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to modify collection:', error); + throw error; + } + } + + /** + * Delete a collection + */ + async function destroy( + collection: CollectionObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await collectionService.destroy({ + provider: collection.provider, + service: collection.service, + identifier: collection.id, + uid + }); + + if (response.success) { + const index = collections.value.findIndex(c => c.id === collection.id); + if (index !== -1) { + collections.value.splice(index, 1); + } + } + + console.debug('[Chrono Manager](Store) - Successfully destroyed collection'); + + return response.success; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to destroy collection:', error); + throw error; + } + } + + return { + // State + collections, + + // Actions + list, + fetch, + fresh, + create, + modify, + destroy, + }; +}); diff --git a/src/stores/entitiesStore.ts b/src/stores/entitiesStore.ts new file mode 100644 index 0000000..3d79dbf --- /dev/null +++ b/src/stores/entitiesStore.ts @@ -0,0 +1,281 @@ +/** + * Chrono Manager - Entities Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { entityService } from '../services/entityService'; +import { EntityObject } from '../models/entity'; +import { EventObject } from '../models/event'; +import { TaskObject } from '../models/task'; +import { JournalObject } from '../models/journal'; +import { CollectionObject } from '../models/collection'; +import type { + SourceSelector, + ListFilter, + ListSort, + ListRange, +} from '../types/common'; +import type { + EntityInterface, +} from '../types/entity'; + +export const useEntitiesStore = defineStore('chronoEntitiesStore', () => { + // State + const entities = ref([]); + + // Actions + + /** + * Reset the store to initial state + */ + function reset(): void { + entities.value = []; + } + + /** + * List entities for all or specific collection + */ + async function list( + provider: string | null, + service: string | null, + collection: string | number | null, + filter?: ListFilter, + sort?: ListSort, + range?: ListRange, + uid?: string + ): Promise { + try { + // Validate hierarchical requirements + if (collection !== null && (service === null || provider === null)) { + throw new Error('Collection requires both service and provider'); + } + if (service !== null && provider === null) { + throw new Error('Service requires provider'); + } + + // Build sources object level by level + const sources: SourceSelector = {}; + if (provider !== null) { + if (service !== null) { + if (collection !== null) { + sources[provider] = { [service]: { [collection]: true } }; + } else { + sources[provider] = { [service]: true }; + } + } else { + sources[provider] = true; + } + } + + // Transmit + const response = await entityService.list({ sources, filter, sort, range, uid }); + + // Flatten the nested response into a flat array + const flatEntities: EntityObject[] = []; + Object.entries(response).forEach(([, providerEntities]) => { + Object.entries(providerEntities).forEach(([, serviceEntities]) => { + Object.entries(serviceEntities).forEach(([, collectionEntities]) => { + Object.values(collectionEntities).forEach((entity: EntityInterface) => { + flatEntities.push(new EntityObject().fromJson(entity)); + }); + }); + }); + }); + + console.debug('[Chrono Manager](Store) - Successfully retrieved', flatEntities.length, 'entities'); + + entities.value = flatEntities; + return flatEntities; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to retrieve entities:', error); + throw error; + } + } + + /** + * Fetch entities for a specific collection + */ + async function fetch( + collection: CollectionObject, + identifiers: (string | number)[], + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await entityService.fetch({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + identifiers, + uid + }); + + return Object.values(response).map(entity => new EntityObject().fromJson(entity)); + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to fetch entities:', error); + throw error; + } + } + + /** + * Create a fresh entity object + */ + function fresh(type: string): EntityObject { + const entity = new EntityObject(); + + if (type === 'event') { + entity.data = new EventObject(); + } else if (type === 'task') { + entity.data = new TaskObject(); + } else if (type === 'journal') { + entity.data = new JournalObject(); + } else { + entity.data = new EventObject(); + } + + if (entity.data) { + entity.data.created = new Date(); + } + + return entity; + } + + /** + * Create a new entity + */ + async function create( + collection: CollectionObject, + entity: EntityObject, + options?: string[], + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await entityService.create({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + data: entity.toJson(), + options, + uid + }); + + const createdEntity = new EntityObject().fromJson(response); + entities.value.push(createdEntity); + + console.debug('[Chrono Manager](Store) - Successfully created entity'); + + return createdEntity; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to create entity:', error); + throw error; + } + } + + /** + * Modify an existing entity + */ + async function modify( + collection: CollectionObject, + entity: EntityObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + if (!entity.in || !entity.id) { + throw new Error('Invalid entity object, must have an collection and entity identifier'); + } + if (collection.id !== entity.in) { + throw new Error('Invalid entity object, does not belong to the specified collection'); + } + + const response = await entityService.modify({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + identifier: entity.id, + data: entity.toJson(), + uid + }); + + const modifiedEntity = new EntityObject().fromJson(response); + const index = entities.value.findIndex(e => e.id === entity.id); + if (index !== -1) { + entities.value[index] = modifiedEntity; + } + + console.debug('[Chrono Manager](Store) - Successfully modified entity'); + + return modifiedEntity; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to modify entity:', error); + throw error; + } + } + + /** + * Delete an entity + */ + async function destroy( + collection: CollectionObject, + entity: EntityObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + if (!entity.in || !entity.id) { + throw new Error('Invalid entity object, must have an collection and entity identifier'); + } + if (collection.id !== entity.in) { + throw new Error('Invalid entity object, does not belong to the specified collection'); + } + + const response = await entityService.destroy({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + identifier: entity.id, + uid + }); + + if (response.success) { + const index = entities.value.findIndex(e => e.id === entity.id); + if (index !== -1) { + entities.value.splice(index, 1); + } + } + + console.debug('[Chrono Manager](Store) - Successfully destroyed entity'); + + return response.success; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to destroy entity:', error); + throw error; + } + } + + return { + // State + entities, + + // Actions + reset, + list, + fetch, + fresh, + create, + modify, + destroy, + }; +}); diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..09a4297 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,8 @@ +/** + * Central export point for all Chrono Manager stores + */ + +export { useCollectionsStore } from './collectionsStore'; +export { useEntitiesStore } from './entitiesStore'; +export { useProvidersStore } from './providersStore'; +export { useServicesStore } from './servicesStore'; diff --git a/src/stores/providersStore.ts b/src/stores/providersStore.ts new file mode 100644 index 0000000..38bbd9f --- /dev/null +++ b/src/stores/providersStore.ts @@ -0,0 +1,62 @@ +/** + * Chrono Manager - Providers Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { providerService } from '../services/providerService'; +import type { + SourceSelector, + ProviderInterface, +} from '../types'; + +export const useProvidersStore = defineStore('chronoProvidersStore', () => { + // State + const providers = ref>({}); + + // Actions + + /** + * List all available providers + * + * @returns Promise with provider list keyed by provider ID + */ + async function list(): Promise> { + try { + const response = await providerService.list(); + + console.debug('[Chrono Manager](Store) - Successfully retrieved', Object.keys(response).length, 'providers:', Object.keys(response)); + + providers.value = response; + return response; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to retrieve providers:', error); + throw error; + } + } + + /** + * Check which providers exist/are available + * + * @param sources - Source selector with provider IDs to check + * @returns Promise with provider availability status + */ + async function extant(sources: SourceSelector): Promise> { + try { + const response = await providerService.extant(sources); + return response; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to check provider existence:', error); + throw error; + } + } + + return { + // State + providers, + + // Actions + list, + extant, + }; +}); diff --git a/src/stores/servicesStore.ts b/src/stores/servicesStore.ts new file mode 100644 index 0000000..0f53f8b --- /dev/null +++ b/src/stores/servicesStore.ts @@ -0,0 +1,95 @@ +/** + * Chrono Manager - Services Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { serviceService } from '../services/serviceService'; +import { ServiceObject } from '../models/service'; +import type { ServiceInterface } from '../types/service'; +import type { + SourceSelector, + ListFilter, + ListSort, +} from '../types/common'; + +export const useServicesStore = defineStore('chronoServicesStore', () => { + // State + const services = ref([]); + // Actions + + /** + * Retrieve services from the server + */ + async function list( + sources?: SourceSelector, + filter?: ListFilter, + sort?: ListSort, + uid?: string + ): Promise { + try { + const response = await serviceService.list({ sources, filter, sort, uid }); + + // Flatten the nested response into a flat array + const flatServices: ServiceObject[] = []; + Object.entries(response).forEach(([providerId, providerServices]) => { + Object.values(providerServices).forEach((service: ServiceInterface) => { + // Ensure provider is set on the service object + service.provider = service.provider || providerId; + flatServices.push(new ServiceObject().fromJson(service)); + }); + }); + + console.debug('[Chrono Manager](Store) - Successfully retrieved', flatServices.length, 'services:', flatServices.map(s => ({ + id: s.id, + label: s.label, + provider: s.provider + }))); + + services.value = flatServices; + return flatServices; + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to retrieve services:', error); + throw error; + } + } + + /** + * Fetch a specific service + * + * @param provider - Provider identifier + * @param identifier - Service identifier + * @param uid - Optional user identifier + * @returns Promise with service object + */ + async function fetch( + provider: string, + identifier: string, + uid?: string + ): Promise { + try { + const response = await serviceService.fetch({ provider, service: identifier, uid }); + return new ServiceObject().fromJson(response); + } catch (error: any) { + console.error('[Chrono Manager](Store) - Failed to fetch service:', error); + throw error; + } + } + + /** + * Create a fresh service object with default values + */ + function fresh(): ServiceObject { + return new ServiceObject(); + } + + return { + // State + services, + + // Actions + list, + fetch, + fresh, + }; +}); diff --git a/src/types/collection.ts b/src/types/collection.ts new file mode 100644 index 0000000..ad75170 --- /dev/null +++ b/src/types/collection.ts @@ -0,0 +1,158 @@ +/** + * Collection-related type definitions for Chrono Manager + */ + +import type { ListFilter, ListSort, SourceSelector } from "./common"; + +/** + * Permission settings for a collection + */ +export interface CollectionPermissionInterface { + view: boolean; + create: boolean; + modify: boolean; + destroy: boolean; + share: boolean; +} + +/** + * Permissions settings for multiple users in a collection + */ +export interface CollectionPermissionsInterface { + [userId: string]: CollectionPermissionInterface; +} + +/** + * Content type settings for a collection + */ +export interface CollectionContentsInterface { + event?: boolean; + task?: boolean; + journal?: boolean; + [contentType: string]: boolean | undefined; +} + +/** + * Represents a collection (calendar) within a service + */ +export interface CollectionInterface { + '@type': string; + provider: string | null; + service: string | null; + in: number | string | null; + id: number | string | null; + label: string | null; + description: string | null; + priority: number | null; + visibility: string | null; + color: string | null; + enabled: boolean; + signature: string | null; + permissions: CollectionPermissionsInterface; + contents: CollectionContentsInterface; +} + +/** + * Request to collection list endpoint + */ +export interface CollectionListRequest { + sources?: SourceSelector; + filter?: ListFilter; + sort?: ListSort; + uid?: string; +} + +/** + * Response from collection list endpoint + */ +export interface CollectionListResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: CollectionInterface; + }; + }; +} + +/** + * Request to collection extant endpoint + */ +export interface CollectionExtantRequest { + sources: SourceSelector; + uid?: string; +} + +/** + * Response from collection extant endpoint + */ +export interface CollectionExtantResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: boolean; + }; + }; +} + +/** + * Request to collection fetch endpoint + */ +export interface CollectionFetchRequest { + provider: string; + service: string; + identifier: string | number; + uid?: string; +} + +/** + * Response from collection fetch endpoint + */ +export interface CollectionFetchResponse extends CollectionInterface {} + +/** + * Request to collection create endpoint + */ +export interface CollectionCreateRequest { + provider: string; + service: string; + data: CollectionInterface; + options?: (string)[] + uid?: string; +} + +/** + * Response from collection create endpoint + */ +export interface CollectionCreateResponse extends CollectionInterface {} + +/** + * Request to collection modify endpoint + */ +export interface CollectionModifyRequest { + provider: string; + service: string; + identifier: string | number; + data: CollectionInterface; + uid?: string; +} + +/** + * Response from collection modify endpoint + */ +export interface CollectionModifyResponse extends CollectionInterface {} + +/** + * Request to collection destroy endpoint + */ +export interface CollectionDestroyRequest { + provider: string; + service: string; + identifier: string | number; + uid?: string; +} + + +/** + * Response from collection destroy endpoint + */ +export interface CollectionDestroyResponse { + success: boolean; +} diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..b9926de --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,65 @@ +/** + * Common types shared across Chrono Manager services + */ + +import type { FilterComparisonOperator, FilterConjunctionOperator } from './service'; + +/** + * Source selector structure for hierarchical resource selection + * Structure: Provider -> Service -> Collection -> Entity + * + * Examples: + * - Simple boolean: { "local": true } + * - Nested services: { "system": { "personal": true, "recents": true } } + * - Collection IDs: { "system": { "personal": { "299": true, "176": true } } } + * - Entity IDs: { "system": { "personal": { "299": [1350, 1353, 5000] } } } + */ +export type SourceSelector = { + [provider: string]: boolean | ServiceSelector; +}; + +export type ServiceSelector = { + [service: string]: boolean | CollectionSelector; +}; + +export type CollectionSelector = { + [collection: string | number]: boolean | EntitySelector; +}; + +export type EntitySelector = (string | number)[]; + +/** + * Filter condition for building complex queries + */ +export interface FilterCondition { + attribute: string; + value: string | number | boolean | any[]; + comparator?: FilterComparisonOperator; + conjunction?: FilterConjunctionOperator; +} + +/** + * Filter criteria for list operations + * Can be simple key-value pairs or complex filter conditions + */ +export interface ListFilter { + label?: string; + [key: string]: any; +} + +/** + * Sort options for list operations + */ +export interface ListSort { + [key: string]: boolean; +} + +/** + * Range specification for pagination/limiting results + */ +export interface ListRange { + type: 'tally'; + anchor: 'absolute' | 'relative'; + position: number; + tally: number; +} diff --git a/src/types/entity.ts b/src/types/entity.ts new file mode 100644 index 0000000..70fc843 --- /dev/null +++ b/src/types/entity.ts @@ -0,0 +1,166 @@ +import type { ListFilter, ListRange, ListSort, SourceSelector } from './common'; +import type { EventInterface } from './event'; +import type { TaskInterface } from './task'; +import type { JournalInterface } from './journal'; + +/** + * Entity-related type definitions for Chrono Manager + */ + +/** + * Represents a chrono entity (event, task, or journal) + */ +export interface EntityInterface { + '@type': string; + version: number; + in: string | number | null; + id: string | number | null; + createdOn: Date | null; + createdBy: string | null; + modifiedOn: Date | null; + modifiedBy: string | null; + signature: string | null; + data: EventInterface | TaskInterface | JournalInterface | null; +} + +/** + * Request to entity list endpoint + */ +export interface EntityListRequest { + sources?: SourceSelector; + filter?: ListFilter; + sort?: ListSort; + range?: ListRange; + uid?: string; +} + +/** + * Response from entity list endpoint + */ +export interface EntityListResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: { + [entityId: string]: EntityInterface; + }; + }; + }; +} + +/** + * Request to entity delta endpoint + */ +export interface EntityDeltaRequest { + sources: SourceSelector; + uid?: string; +} + +/** + * Response from entity delta endpoint + */ +export interface EntityDeltaResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: { + signature: string; + created?: { + [entityId: string]: EntityInterface; + }; + modified?: { + [entityId: string]: EntityInterface; + }; + deleted?: string[]; // Array of deleted entity IDs + }; + }; + }; +} + +/** + * Request to entity extant endpoint + */ +export interface EntityExtantRequest { + sources: SourceSelector; + uid?: string; +} + +/** + * Response from entity extant endpoint + */ +export interface EntityExtantResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: { + [entityId: string]: boolean; + }; + }; + }; +} + +/** + * Request to entity fetch endpoint + */ +export interface EntityFetchRequest { + provider: string; + service: string; + collection: string | number; + identifiers: (string | number)[]; + uid?: string; +} + +/** + * Response from entity fetch endpoint + */ +export interface EntityFetchResponse extends Record {} + +/** + * Request to entity create endpoint + */ +export interface EntityCreateRequest { + provider: string; + service: string; + collection: string | number; + data: EntityInterface; + options?: (string)[] + uid?: string; +} + +/** + * Response from entity create endpoint + */ +export interface EntityCreateResponse extends EntityInterface {} + +/** + * Request to entity modify endpoint + */ +export interface EntityModifyRequest { + provider: string; + service: string; + collection: string | number; + identifier: string | number; + data: EntityInterface; + options?: (string)[] + uid?: string; +} + +/** + * Response from entity modify endpoint + */ +export interface EntityModifyResponse extends EntityInterface {} + +/** + * Request to entity destroy endpoint + */ +export interface EntityDestroyRequest { + provider: string; + service: string; + collection: string | number; + identifier: string | number; + uid?: string; +} + +/** + * Response from entity destroy endpoint + */ +export interface EntityDestroyResponse { + success: boolean; +} diff --git a/src/types/event.ts b/src/types/event.ts new file mode 100644 index 0000000..6c579f5 --- /dev/null +++ b/src/types/event.ts @@ -0,0 +1,146 @@ +/** + * Event-related type definitions for Chrono Manager + */ + +/** + * Physical location information + */ +export interface EventLocationPhysicalInterface { + identifier: string | null; + label?: string | null; + description?: string | null; + relation?: 'start' | 'end' | null; + timeZone?: string | null; +} + +/** + * Virtual location information + */ +export interface EventLocationVirtualInterface { + identifier: string | null; + location: string; + label?: string | null; + description?: string | null; + relation?: string | null; +} + +/** + * Event organizer information + */ +export interface EventOrganizerInterface { + realm: 'I' | 'E'; // I = Internal, E = External + address: string; + name?: string | null; +} + +/** + * Event participant/attendee information + */ +export interface EventParticipantInterface { + identifier: string | null; + realm: 'I' | 'E'; // I = Internal, E = External + address: string; + type: 'unknown' | 'individual' | 'group' | 'resource' | 'location'; + status: 'none' | 'accepted' | 'declined' | 'tentative' | 'delegated'; + roles: ('owner' | 'chair' | 'attendee' | 'optional' | 'informational' | 'contact')[]; + name?: string | null; + description?: string | null; + comment?: string | null; + language?: string | null; +} + +/** + * Event notification/alarm + */ +export interface EventNotificationInterface { + identifier: string | null; + type: 'visual' | 'audible' | 'email'; + pattern: 'absolute' | 'relative' | 'unknown'; + when?: string | null; // ISO 8601 date-time string + anchor?: 'start' | 'end' | null; + offset?: string | null; // ISO 8601 duration string +} + +/** + * Event occurrence/recurrence pattern + */ +export interface EventOccurrence { + pattern: 'absolute' | 'relative'; + precision: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly' ; + interval: number; + iterations?: number | null; + concludes?: string | null; // ISO 8601 date-time string + scale?: string | null; + onDayOfWeek?: number[] | null; + onDayOfMonth?: number[] | null; + onDayOfYear?: number[] | null; + onWeekOfMonth?: number[] | null; + onWeekOfYear?: number[] | null; + onMonthOfYear?: number[] | null; + onHour?: number[] | null; + onMinute?: number[] | null; + onSecond?: number[] | null; + onPosition?: number[] | null; +} + +/** + * Event mutation (exception/modification to recurring event) + */ +export interface EventMutation { + mutationId: string | null; // ISO 8601 date-time string + mutationTz: string | null; + mutationExclusion: boolean | null; + sequence: number | null; + timeZone: string | null; + startsOn: string | null; // ISO 8601 date-time string + startsTZ: string | null; + endsOn: string | null; // ISO 8601 date-time string + endsTZ: string | null; + duration: string | null; // ISO 8601 duration string + timeless: boolean | null; + label: string | null; + description: string | null; + locationsPhysical: Record; + locationsVirtual: Record; + availability: 'free' | 'busy' | null; + priority: number | null; + sensitivity: 'public' | 'private' | 'secret' | null; + color: string | null; + tags: string[]; + organizer: EventOrganizerInterface | null; + participants: Record; + notifications: Record; +} + +/** + * Main event interface + */ +export interface EventInterface { + type: string; + version: number; + urid: string | null; + created: string | null; // ISO 8601 date-time string + modified: string | null; // ISO 8601 date-time string + sequence: number | null; + timeZone: string | null; + startsOn: string | null; // ISO 8601 date-time string + startsTZ: string | null; + endsOn: string | null; // ISO 8601 date-time string + endsTZ: string | null; + duration: string | null; // ISO 8601 duration string + timeless: boolean | null; + label: string | null; + description: string | null; + locationsPhysical: Record; + locationsVirtual: Record; + availability: 'free' | 'busy' | null; + sensitivity: 'public' | 'private' | 'secret' | null; + priority: number | null; + color: string | null; + tags: string[]; + organizer: EventOrganizerInterface | null; + participants: Record; + notifications: Record; + pattern: EventOccurrence | null; + mutations: Record; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..87db923 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,12 @@ +/** + * Central export point for all Chrono Manager types + */ + +export type * from './collection'; +export type * from './common'; +export type * from './entity'; +export type * from './event'; +export type * from './task'; +export type * from './journal'; +export type * from './provider'; +export type * from './service'; diff --git a/src/types/journal.ts b/src/types/journal.ts new file mode 100644 index 0000000..6567dee --- /dev/null +++ b/src/types/journal.ts @@ -0,0 +1,33 @@ +/** + * Journal-related type definitions for Chrono Manager + */ + +/** + * Journal attachment + */ +export interface JournalAttachment { + uri: string | null; + type: string | null; + label: string | null; + priority: number | null; +} + +/** + * Data for a journal entity + */ +export interface JournalInterface { + type: string; + version: number; + urid: string | null; + created: Date | null; + modified: Date | null; + label: string | null; + description: string | null; + content: string | null; + startsOn: Date | null; + endsOn: Date | null; + status: string | null; // 'draft', 'final', 'cancelled' + visibility: string | null; // 'public', 'private', 'confidential' + attachments: Record; + tags: string[]; +} diff --git a/src/types/provider.ts b/src/types/provider.ts new file mode 100644 index 0000000..2db5be0 --- /dev/null +++ b/src/types/provider.ts @@ -0,0 +1,53 @@ +/** + * Provider-specific types + */ +import type { SourceSelector } from "./common"; + +/** + * Provider capabilities + */ +export interface ProviderCapabilitiesInterface { + ServiceList?: boolean; + ServiceFetch?: boolean; + ServiceExtant?: boolean; + ServiceCreate?: boolean; + ServiceModify?: boolean; + ServiceDelete?: boolean; + [key: string]: boolean | undefined; +} + +/** + * Provider information + */ +export interface ProviderInterface { + '@type': string; + id: string; + label: string; + capabilities: ProviderCapabilitiesInterface; +} + +/** + * Request to provider list endpoint + */ +export interface ProviderListRequest {} + +/** + * Response from provider list endpoint + */ +export interface ProviderListResponse { + [providerId: string]: ProviderInterface; +} + +/** + * Request to provider extant endpoint + */ +export interface ProviderExtantRequest { + sources: SourceSelector; +} + +/** + * Response from provider extant endpoint + */ +export interface ProviderExtantResponse { + [providerId: string]: boolean; +} diff --git a/src/types/service.ts b/src/types/service.ts new file mode 100644 index 0000000..47803af --- /dev/null +++ b/src/types/service.ts @@ -0,0 +1,214 @@ +/** + * Service-related type definitions for Chrono Manager + */ + +import type { ListFilter, ListSort, SourceSelector } from "./common"; + +/** + * Filter comparison operators (bitmask values) + */ +export const FilterComparisonOperator = { + EQ: 1, // Equal + NEQ: 2, // Not Equal + GT: 4, // Greater Than + LT: 8, // Less Than + GTE: 16, // Greater Than or Equal + LTE: 32, // Less Than or Equal + IN: 64, // In Array + NIN: 128, // Not In Array + LIKE: 256, // Like (pattern matching) + NLIKE: 512, // Not Like +} as const; + +export type FilterComparisonOperator = typeof FilterComparisonOperator[keyof typeof FilterComparisonOperator]; + +/** + * Filter conjunction operators + */ +export const FilterConjunctionOperator = { + NONE: '', + AND: 'AND', + OR: 'OR', +} as const; + +export type FilterConjunctionOperator = typeof FilterConjunctionOperator[keyof typeof FilterConjunctionOperator]; + +/** + * Filter specification format + * Format: "type:length:defaultComparator:supportedComparators" + * + * Examples: + * - "s:200:256:771" = String field, max 200 chars, default LIKE, supports EQ|NEQ|LIKE|NLIKE + * - "a:10:64:192" = Array field, max 10 items, default IN, supports IN|NIN + * - "i:0:1:31" = Integer field, default EQ, supports EQ|NEQ|GT|LT|GTE|LTE + * + * Type codes: + * - s = string + * - i = integer + * - b = boolean + * - a = array + * + * Comparator values are bitmasks that can be combined + */ +export type FilterSpec = string; + +/** + * Parsed filter specification + */ +export interface ParsedFilterSpec { + type: 'string' | 'integer' | 'boolean' | 'array'; + length: number; + defaultComparator: FilterComparisonOperator; + supportedComparators: FilterComparisonOperator[]; +} + +/** + * Parse a filter specification string into its components + * + * @param spec - Filter specification string (e.g., "s:200:256:771") + * @returns Parsed filter specification object + * + * @example + * parseFilterSpec("s:200:256:771") + * // Returns: { + * // type: 'string', + * // length: 200, + * // defaultComparator: 256 (LIKE), + * // supportedComparators: [1, 2, 256, 512] (EQ, NEQ, LIKE, NLIKE) + * // } + */ +export function parseFilterSpec(spec: FilterSpec): ParsedFilterSpec { + const [typeCode, lengthStr, defaultComparatorStr, supportedComparatorsStr] = spec.split(':'); + + const typeMap: Record = { + 's': 'string', + 'i': 'integer', + 'b': 'boolean', + 'a': 'array', + }; + + const type = typeMap[typeCode]; + if (!type) { + throw new Error(`Invalid filter type code: ${typeCode}`); + } + + const length = parseInt(lengthStr, 10); + const defaultComparator = parseInt(defaultComparatorStr, 10) as FilterComparisonOperator; + + // Parse supported comparators from bitmask + const supportedComparators: FilterComparisonOperator[] = []; + const supportedBitmask = parseInt(supportedComparatorsStr, 10); + + if (supportedBitmask !== 0) { + const allComparators = Object.values(FilterComparisonOperator).filter(v => typeof v === 'number') as number[]; + for (const comparator of allComparators) { + if ((supportedBitmask & comparator) === comparator) { + supportedComparators.push(comparator as FilterComparisonOperator); + } + } + } + + return { + type, + length, + defaultComparator, + supportedComparators, + }; +} + +/** + * Capabilities available for a service + */ +export interface ServiceCapabilitiesInterface { + // Collection capabilities + CollectionList?: boolean; + CollectionListFilter?: { + [key: string]: FilterSpec; + }; + CollectionListSort?: string[]; + CollectionExtant?: boolean; + CollectionFetch?: boolean; + CollectionCreate?: boolean; + CollectionModify?: boolean; + CollectionDestroy?: boolean; + + // Entity capabilities + EntityList?: boolean; + EntityListFilter?: { + [key: string]: FilterSpec; + }; + EntityListSort?: string[]; + EntityListRange?: { + [rangeType: string]: string[]; // e.g., { "tally": ["absolute", "relative"] } + }; + EntityDelta?: boolean; + EntityExtant?: boolean; + EntityFetch?: boolean; + EntityCreate?: boolean; + EntityModify?: boolean; + EntityDestroy?: boolean; + EntityCopy?: boolean; + EntityMove?: boolean; +} + +/** + * Represents a service within a provider + */ +export interface ServiceInterface { + '@type': string; + provider: string; + id: string; + label: string; + capabilities?: ServiceCapabilitiesInterface; + enabled: boolean; +} + +/** + * Request to service list endpoint + */ +export interface ServiceListRequest { + sources?: SourceSelector; + filter?: ListFilter; + sort?: ListSort; + uid?: string; +} + +/** + * Response from service list endpoint + */ +export interface ServiceListResponse { + [providerId: string]: { + [serviceId: string]: ServiceInterface; + }; +} + +/** + * Request to service extant endpoint + */ +export interface ServiceExtantRequest { + sources: SourceSelector; + uid?: string; +} + +/** + * Response from service extant endpoint + */ +export interface ServiceExtantResponse { + [providerId: string]: { + [serviceId: string]: boolean; + }; +} + +/** + * Request to service fetch endpoint + */ +export interface ServiceFetchRequest { + provider: string; + service: string; + uid?: string; +} + +/** + * Response from service fetch endpoint + */ +export interface ServiceFetchResponse extends ServiceInterface {} diff --git a/src/types/task.ts b/src/types/task.ts new file mode 100644 index 0000000..73b8621 --- /dev/null +++ b/src/types/task.ts @@ -0,0 +1,60 @@ +/** + * Task-related type definitions for Chrono Manager + */ + +/** + * Task subtask information + */ +export interface TaskSubtask { + label: string | null; + completed: boolean; + priority: number | null; +} + +/** + * Task attachment + */ +export interface TaskAttachment { + uri: string | null; + type: string | null; + label: string | null; + priority: number | null; +} + +/** + * Task recurrence rule + */ +export interface TaskRecurrence { + frequency: string | null; // 'daily', 'weekly', 'monthly', 'yearly' + interval: number | null; + count: number | null; + until: Date | null; + byDay: string[] | null; + byMonthDay: number[] | null; + byMonth: number[] | null; +} + +/** + * Data for a task entity + */ +export interface TaskInterface { + type: string; + version: number; + urid: string | null; + created: Date | null; + modified: Date | null; + label: string | null; + description: string | null; + startsOn: Date | null; + dueOn: Date | null; + completedOn: Date | null; + status: string | null; // 'needs-action', 'in-process', 'completed', 'cancelled' + priority: number | null; + progress: number | null; // 0-100 percentage + color: string | null; + recurrence: TaskRecurrence | null; + subtasks: Record; + attachments: Record; + tags: string[]; + notes: string | null; +} diff --git a/src/utils/key-generator.ts b/src/utils/key-generator.ts new file mode 100644 index 0000000..2422c71 --- /dev/null +++ b/src/utils/key-generator.ts @@ -0,0 +1,31 @@ +/** + * Utility functions for generating unique identifiers + */ + +const globalCrypto = typeof globalThis !== "undefined" ? globalThis.crypto : undefined; + +export const generateUrid = (): string => { + if (globalCrypto?.randomUUID) { + return globalCrypto.randomUUID(); + } + + const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; + return template.replace(/[xy]/g, char => { + const randomNibble = Math.floor(Math.random() * 16); + const value = char === "x" ? randomNibble : (randomNibble & 0x3) | 0x8; + return value.toString(16); + }); +}; + +export const generateKey = (): string => { + if (globalCrypto?.randomUUID) { + return globalCrypto.randomUUID().replace(/-/g, ""); + } + + if (globalCrypto?.getRandomValues) { + const [value] = globalCrypto.getRandomValues(new Uint32Array(1)); + return value.toString(16); + } + + return Math.random().toString(16).slice(2); +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..88d256c --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,19 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"], + "@KTXC/*": ["../../core/src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..43a7d67 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vite' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@KTXC': path.resolve(__dirname, '../../core/src') + }, + }, + build: { + outDir: 'static', + sourcemap: true, + lib: { + entry: path.resolve(__dirname, 'src/main.ts'), + formats: ['es'], + fileName: () => 'module.mjs', + }, + rollupOptions: { + external: [ + 'pinia', + 'vue', + 'vue-router', + // Externalize shared utilities from core to avoid duplication + /^@KTXC\/utils\//, + ], + }, + }, +})