From 99474ccd75e7ec2c3d998bd753be20c06f81fd1a Mon Sep 17 00:00:00 2001 From: Chris Rosenau Date: Wed, 12 Nov 2025 17:52:54 -0700 Subject: [PATCH] broke out code --- .cursor/rules/migrate.mdc | 6 + app/Http/Controllers/AttributesController.php | 80 + app/Http/Controllers/CategoriesController.php | 228 ++ .../Controllers/ConnectionsController.php | 42 + app/Http/Controllers/MigrationController.php | 71 + app/Http/Controllers/ProductsController.php | 127 + package-lock.json | 2378 ++++++++++++++ resources/css/app.css | 6 + resources/css/attributes.css | 31 + resources/css/base.css | 239 ++ resources/css/categories.css | 313 ++ resources/css/connections.css | 27 + resources/css/migration.css | 30 + resources/css/products.css | 31 + resources/js/attributes.js | 118 + resources/js/categories.js | 268 ++ resources/js/connections.js | 28 + resources/js/migration.js | 101 + resources/js/products.js | 358 +++ resources/views/attributes/index.blade.php | 274 ++ resources/views/categories/index.blade.php | 123 + resources/views/connections/index.blade.php | 40 + resources/views/layouts/app.blade.php | 28 + resources/views/migration/index.blade.php | 2860 +---------------- resources/views/partials/navigation.blade.php | 18 + resources/views/products/index.blade.php | 234 ++ routes/web.php | 65 +- vite.config.js | 10 +- 28 files changed, 5358 insertions(+), 2776 deletions(-) create mode 100644 .cursor/rules/migrate.mdc create mode 100644 app/Http/Controllers/AttributesController.php create mode 100644 app/Http/Controllers/CategoriesController.php create mode 100644 app/Http/Controllers/ConnectionsController.php create mode 100644 app/Http/Controllers/MigrationController.php create mode 100644 app/Http/Controllers/ProductsController.php create mode 100644 package-lock.json create mode 100644 resources/css/attributes.css create mode 100644 resources/css/base.css create mode 100644 resources/css/categories.css create mode 100644 resources/css/connections.css create mode 100644 resources/css/migration.css create mode 100644 resources/css/products.css create mode 100644 resources/js/attributes.js create mode 100644 resources/js/categories.js create mode 100644 resources/js/connections.js create mode 100644 resources/js/migration.js create mode 100644 resources/js/products.js create mode 100644 resources/views/attributes/index.blade.php create mode 100644 resources/views/categories/index.blade.php create mode 100644 resources/views/connections/index.blade.php create mode 100644 resources/views/layouts/app.blade.php create mode 100644 resources/views/partials/navigation.blade.php create mode 100644 resources/views/products/index.blade.php diff --git a/.cursor/rules/migrate.mdc b/.cursor/rules/migrate.mdc new file mode 100644 index 0000000..a0f32c9 --- /dev/null +++ b/.cursor/rules/migrate.mdc @@ -0,0 +1,6 @@ +--- +alwaysApply: true +--- +use tailwind standards for css +use blade partials for reusable components +tabs should be specific pages with specific controllers diff --git a/app/Http/Controllers/AttributesController.php b/app/Http/Controllers/AttributesController.php new file mode 100644 index 0000000..1369899 --- /dev/null +++ b/app/Http/Controllers/AttributesController.php @@ -0,0 +1,80 @@ +migrationService = $migrationService; + } + + /** + * Show the attributes page + */ + public function index() + { + $m1Attributes = $this->migrationService->getMagento1Attributes(); + $m2Attributes = $this->migrationService->getMagento2Attributes(); + $m1AttributesMissingInM2 = $this->migrationService->getM1AttributesMissingInM2(); + $m1AttributeGroups = $this->migrationService->getMagento1AttributeGroups(); + $m2AttributeGroups = $this->migrationService->getMagento2AttributeGroups(); + $m1AttributeGroupsMissingInM2 = $this->migrationService->getM1AttributeGroupsMissingInM2(); + + return view('attributes.index', [ + 'm1Attributes' => $m1Attributes, + 'm2Attributes' => $m2Attributes, + 'm1AttributesMissingInM2' => $m1AttributesMissingInM2, + 'm1AttributeGroups' => $m1AttributeGroups, + 'm2AttributeGroups' => $m2AttributeGroups, + 'm1AttributeGroupsMissingInM2' => $m1AttributeGroupsMissingInM2, + ]); + } + + /** + * Migrate an attribute from Magento 1 to Magento 2 + */ + public function migrateAttribute(Request $request, $attributeId) + { + try { + $result = $this->migrationService->migrateAttribute($attributeId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Attribute migration error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + ], 500); + } + } + + /** + * Migrate an attribute group from Magento 1 to Magento 2 + */ + public function migrateAttributeGroup(Request $request, $groupId, $setId) + { + try { + $result = $this->migrationService->migrateAttributeGroup($groupId, $setId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Attribute group migration error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + ], 500); + } + } +} + diff --git a/app/Http/Controllers/CategoriesController.php b/app/Http/Controllers/CategoriesController.php new file mode 100644 index 0000000..4b52e21 --- /dev/null +++ b/app/Http/Controllers/CategoriesController.php @@ -0,0 +1,228 @@ +migrationService = $migrationService; + } + + /** + * Show the categories page + */ + public function index() + { + $m1Categories = $this->migrationService->getMagento1Categories(); + $m2Categories = $this->migrationService->getMagento2Categories(); + $m2CategoriesNotInM1 = $this->migrationService->getM2CategoriesNotInM1(); + + return view('categories.index', [ + 'm1CategoriesCount' => $m1Categories->count(), + 'm2CategoriesCount' => $m2Categories->count(), + 'm2CategoriesNotInM1' => $m2CategoriesNotInM1, + ]); + } + + /** + * Get Magento 1 categories preview + */ + public function getMagento1Categories() + { + $categories = $this->migrationService->getMagento1Categories(); + + return response()->json([ + 'success' => true, + 'count' => $categories->count(), + 'categories' => $categories->take(50)->map(function($cat) { + return [ + 'id' => $cat->entity_id, + 'name' => $cat->name ?? 'N/A', + 'level' => $cat->level, + 'parent_id' => $cat->parent_id, + 'is_active' => $cat->is_active ?? 0, + ]; + }), + ]); + } + + /** + * Get Magento 1 category tree + */ + public function getMagento1CategoryTree() + { + try { + $categories = $this->migrationService->getMagento1Categories(); + $tree = $this->migrationService->buildCategoryTreeHierarchy($categories, 'm1'); + + return response()->json([ + 'success' => true, + 'tree' => $tree, + ]); + } catch (\Exception $e) { + Log::error('Error fetching M1 category tree: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch category tree: ' . $e->getMessage(), + ], 500); + } + } + + /** + * Get Magento 1 category tree with products + */ + public function getMagento1CategoryTreeWithProducts() + { + try { + $tree = $this->migrationService->getMagento1CategoryTreeWithProducts(); + + return response()->json([ + 'success' => true, + 'tree' => $tree + ]); + } catch (\Exception $e) { + Log::error('Error fetching M1 category tree with products: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to load category tree with products: ' . $e->getMessage(), + 'tree' => [] + ], 500); + } + } + + /** + * Get Magento 2 category tree + */ + public function getMagento2CategoryTree() + { + try { + $categories = $this->migrationService->getMagento2Categories(); + $tree = $this->migrationService->buildCategoryTreeHierarchy($categories, 'm2'); + + return response()->json([ + 'success' => true, + 'tree' => $tree, + ]); + } catch (\Exception $e) { + Log::error('Error fetching M2 category tree: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch category tree: ' . $e->getMessage(), + ], 500); + } + } + + /** + * Get Magento 2 category tree with products + */ + public function getMagento2CategoryTreeWithProducts() + { + try { + $tree = $this->migrationService->getMagento2CategoryTreeWithProducts(); + + return response()->json([ + 'success' => true, + 'tree' => $tree + ]); + } catch (\Exception $e) { + Log::error('Error fetching M2 category tree with products: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to load category tree with products: ' . $e->getMessage(), + 'tree' => [] + ], 500); + } + } + + /** + * Get M2 categories not in M1 (for AJAX refresh) + */ + public function getM2CategoriesNotInM1() + { + try { + $categories = $this->migrationService->getM2CategoriesNotInM1(); + + return response()->json([ + 'success' => true, + 'categories' => $categories->map(function($category) { + return [ + 'entity_id' => $category->entity_id, + 'name' => $category->name ?? 'Unnamed Category', + 'level' => $category->level ?? 'N/A', + 'is_active' => $category->is_active ?? 0, + 'root_category_name' => $category->root_category_name ?? 'N/A', + 'root_category_id' => $category->root_category_id ?? null, + 'path' => $category->path ?? 'N/A', + ]; + }), + 'count' => $categories->count(), + ]); + } catch (\Exception $e) { + Log::error('Error fetching M2 categories not in M1: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch categories: ' . $e->getMessage(), + ], 500); + } + } + + /** + * Delete a category + */ + public function deleteCategory(Request $request, $categoryId) + { + $request->validate([ + 'source' => 'nullable|in:m1,m2', + ]); + + try { + $source = $request->input('source', 'm2'); + $result = $this->migrationService->deleteCategory($categoryId, $source); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Category deletion error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage(), + ], 500); + } + } + + /** + * Rename a Magento 2 category + */ + public function renameCategory(Request $request, $categoryId) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'store_id' => 'nullable|integer', + ]); + + try { + $newName = $request->input('name'); + $storeId = $request->input('store_id', 0); + $result = $this->migrationService->renameCategory($categoryId, $newName, $storeId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Category rename error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Rename failed: ' . $e->getMessage(), + ], 500); + } + } +} + diff --git a/app/Http/Controllers/ConnectionsController.php b/app/Http/Controllers/ConnectionsController.php new file mode 100644 index 0000000..5e060d0 --- /dev/null +++ b/app/Http/Controllers/ConnectionsController.php @@ -0,0 +1,42 @@ +migrationService = $migrationService; + } + + /** + * Show the database connections page + */ + public function index() + { + $connectionTest = $this->migrationService->testConnections(); + + return view('connections.index', [ + 'connectionTest' => $connectionTest, + ]); + } + + /** + * Test database connections + */ + public function testConnections() + { + $results = $this->migrationService->testConnections(); + + return response()->json([ + 'success' => $results['magento1'] && $results['magento2'], + 'results' => $results, + ]); + } +} + diff --git a/app/Http/Controllers/MigrationController.php b/app/Http/Controllers/MigrationController.php new file mode 100644 index 0000000..c7fcb15 --- /dev/null +++ b/app/Http/Controllers/MigrationController.php @@ -0,0 +1,71 @@ +migrationService = $migrationService; + } + + /** + * Show the category migration page + */ + public function index() + { + $m1Stores = $this->migrationService->getMagento1Stores(); + $m2Stores = $this->migrationService->getMagento2Stores(); + $connectionTest = $this->migrationService->testConnections(); + $m1Categories = $this->migrationService->getMagento1Categories(); + $m2Categories = $this->migrationService->getMagento2Categories(); + + return view('migration.index', [ + 'm1Stores' => $m1Stores, + 'm2Stores' => $m2Stores, + 'connectionTest' => $connectionTest, + 'm1CategoriesCount' => $m1Categories->count(), + 'm2CategoriesCount' => $m2Categories->count(), + ]); + } + + /** + * Execute the migration + */ + public function migrate(Request $request) + { + $request->validate([ + 'store_mapping' => 'required|array', + 'store_mapping.*' => 'required|integer', + ]); + + try { + $storeMapping = $request->input('store_mapping'); + + // Convert array format: ['m1_store_id' => 'm2_store_id'] + $mapping = []; + foreach ($storeMapping as $m1StoreId => $m2StoreId) { + $mapping[(int)$m1StoreId] = (int)$m2StoreId; + } + + $result = $this->migrationService->migrateCategories($mapping); + + return response()->json($result); + + } catch (\Exception $e) { + Log::error('Migration error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + ], 500); + } + } +} + diff --git a/app/Http/Controllers/ProductsController.php b/app/Http/Controllers/ProductsController.php new file mode 100644 index 0000000..7ec6a73 --- /dev/null +++ b/app/Http/Controllers/ProductsController.php @@ -0,0 +1,127 @@ +migrationService = $migrationService; + } + + /** + * Show the products page + */ + public function index() + { + $m1Products = $this->migrationService->getMagento1Products(); + $m2Products = $this->migrationService->getMagento2Products(); + $m1ProductsNotInM2 = $this->migrationService->getM1ProductsNotInM2(); + $m2ProductsNotInM1 = $this->migrationService->getM2ProductsNotInM1(); + + return view('products.index', [ + 'm1Products' => $m1Products, + 'm2Products' => $m2Products, + 'm1ProductsNotInM2' => $m1ProductsNotInM2, + 'm2ProductsNotInM1' => $m2ProductsNotInM1, + ]); + } + + /** + * Migrate all products from Magento 1 to Magento 2 + */ + public function migrateProducts(Request $request) + { + try { + $dryRun = $request->input('dry_run', false); + $result = $this->migrationService->migrateProducts($dryRun); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Product migration error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [], + 'missing_attributes' => [] + ], 500); + } + } + + /** + * Delete a single product from Magento 2 + */ + public function deleteM2Product(Request $request, $productId) + { + try { + $result = $this->migrationService->deleteM2Product($productId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Delete M2 product error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage() + ], 500); + } + } + + /** + * Sync product category assignments from M1 to M2 + */ + public function syncProductCategories(Request $request) + { + try { + $result = $this->migrationService->syncProductCategories(); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Sync product categories error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Sync failed: ' . $e->getMessage(), + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ], 500); + } + } + + /** + * Delete all products in Magento 2 that have entity_id greater than max M1 product ID + */ + public function deleteM2ProductsAboveM1Max(Request $request) + { + try { + $result = $this->migrationService->deleteM2ProductsAboveM1Max(); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Delete M2 products error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage(), + 'deleted' => 0 + ], 500); + } + } +} + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5333a69 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2378 @@ +{ + "name": "migrate", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.7" + } + }, + "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/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", + "integrity": "sha512-zQuvzWfUKQu9oNVi1o0RZAJCwhGsdhx4NEOyrVQwJHaWDseGP9tl7XUPLY2T8Cj6+IrZ6lmyxlR1KC8unf3RLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^7.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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==", + "dev": true, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "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.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "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/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/resources/css/app.css b/resources/css/app.css index 3e6abea..8246030 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,4 +1,10 @@ @import 'tailwindcss'; +@import './base.css'; +@import './connections.css'; +@import './categories.css'; +@import './migration.css'; +@import './attributes.css'; +@import './products.css'; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../storage/framework/views/*.php'; diff --git a/resources/css/attributes.css b/resources/css/attributes.css new file mode 100644 index 0000000..a717fc8 --- /dev/null +++ b/resources/css/attributes.css @@ -0,0 +1,31 @@ +/* Attributes page specific styles */ +.attributes-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 6px; +} + +.attributes-table thead tr { + background: #f8f9fa; + border-bottom: 2px solid #dee2e6; +} + +.attributes-table th { + padding: 10px; + text-align: left; + font-weight: 600; + color: #333; + font-size: 0.9em; +} + +.attributes-table td { + padding: 8px 10px; + font-size: 0.85em; + border-bottom: 1px solid #e0e0e0; +} + +.attributes-table tbody tr:hover { + background: #f8f9fa; +} + diff --git a/resources/css/base.css b/resources/css/base.css new file mode 100644 index 0000000..916d14b --- /dev/null +++ b/resources/css/base.css @@ -0,0 +1,239 @@ +@import 'tailwindcss'; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + width: 90%; + max-width: 90%; + margin: 0 auto; + background: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + overflow: hidden; +} + +.header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px; + text-align: center; +} + +.header h1 { + font-size: 2.5em; + margin-bottom: 10px; +} + +.header p { + opacity: 0.9; + font-size: 1.1em; +} + +.content { + padding: 30px; +} + +.section { + margin-bottom: 30px; + padding: 20px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid #667eea; +} + +.section h2 { + color: #333; + margin-bottom: 15px; + font-size: 1.5em; +} + +.btn { + padding: 12px 30px; + border: none; + border-radius: 6px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.btn-secondary { + background: #6c757d; + color: white; +} + +.btn-secondary:hover { + background: #5a6268; +} + +.btn-danger { + background: #d32f2f; + color: white; +} + +.btn-danger:hover { + background: #b71c1c; +} + +.actions { + display: flex; + gap: 15px; + margin-top: 20px; +} + +.info-box { + background: #e7f3ff; + border-left: 4px solid #2196F3; + padding: 15px; + border-radius: 4px; + margin-top: 15px; +} + +.info-box p { + margin: 5px 0; + color: #1976D2; +} + +.loading { + display: none; + text-align: center; + padding: 20px; +} + +.loading.active { + display: block; +} + +.spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-top: 15px; +} + +.stat-card { + background: white; + padding: 20px; + border-radius: 8px; + text-align: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.stat-card .number { + font-size: 2em; + font-weight: bold; + color: #667eea; +} + +.stat-card .label { + color: #666; + margin-top: 5px; +} + +.log-container { + background: #1e1e1e; + color: #d4d4d4; + padding: 20px; + border-radius: 6px; + max-height: 400px; + overflow-y: auto; + font-family: 'Courier New', monospace; + font-size: 0.9em; + margin-top: 15px; + display: none; +} + +.log-container.active { + display: block; +} + +.log-entry { + margin-bottom: 5px; + padding: 5px 0; +} + +.log-entry.error { + color: #f48771; +} + +.log-entry.success { + color: #4ec9b0; +} + +/* Navigation */ +.navigation { + display: flex; + border-bottom: 2px solid #ddd; + margin-bottom: 20px; + background: #f8f9fa; + border-radius: 8px 8px 0 0; +} + +.nav-link { + padding: 15px 30px; + text-decoration: none; + border: none; + background: transparent; + font-size: 1em; + font-weight: 600; + color: #666; + transition: all 0.3s; + border-bottom: 3px solid transparent; + margin-bottom: -2px; +} + +.nav-link:hover { + background: #e9ecef; + color: #333; +} + +.nav-link.active { + color: #667eea; + border-bottom-color: #667eea; + background: white; +} + diff --git a/resources/css/categories.css b/resources/css/categories.css new file mode 100644 index 0000000..a76bc2a --- /dev/null +++ b/resources/css/categories.css @@ -0,0 +1,313 @@ +/* Categories page specific styles */ + +/* Tree View */ +.tree-container { + background: white; + border: 1px solid #ddd; + border-radius: 6px; + padding: 20px; + max-height: 600px; + overflow-y: auto; +} + +.tree-node { + margin: 5px 0; +} + +.tree-node-item { + display: flex; + align-items: center; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + user-select: none; + transition: background 0.2s; +} + +.tree-node-item:hover { + background: #f0f0f0; +} + +.tree-toggle { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 8px; + font-size: 12px; + color: #666; + cursor: pointer; +} + +.tree-toggle.expanded::before { + content: '▼'; +} + +.tree-toggle.collapsed::before { + content: '▶'; +} + +.tree-toggle.leaf { + width: 20px; + margin-right: 8px; +} + +.tree-label { + flex: 1; + display: flex; + align-items: center; + gap: 10px; +} + +.tree-label-text { + font-weight: 500; + color: #333; +} + +.tree-badge { + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75em; + font-weight: 600; +} + +.tree-badge.active { + background: #d4edda; + color: #155724; +} + +.tree-badge.inactive { + background: #f8d7da; + color: #721c24; +} + +.tree-children { + margin-left: 28px; + border-left: 2px solid #e0e0e0; + padding-left: 12px; + display: none; +} + +.tree-children.expanded { + display: block; +} + +.tree-loading { + text-align: center; + padding: 40px; + color: #666; +} + +.tree-error { + background: #f8d7da; + color: #721c24; + padding: 15px; + border-radius: 6px; + margin: 10px 0; +} + +.tree-delete-btn { + background: #dc3545; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + font-size: 0.75em; + cursor: pointer; + margin-left: 8px; + transition: background 0.2s; +} + +.tree-delete-btn:hover { + background: #c82333; +} + +.tree-delete-btn:disabled { + background: #ccc; + cursor: not-allowed; +} + +.tree-rename-btn { + background: #28a745; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + font-size: 0.75em; + cursor: pointer; + margin-left: 8px; + transition: background 0.2s; +} + +.tree-rename-btn:hover { + background: #218838; +} + +.tree-rename-input { + padding: 4px 8px; + border: 2px solid #28a745; + border-radius: 4px; + font-size: 0.9em; + width: 200px; + margin-left: 8px; +} + +.tree-rename-input:focus { + outline: none; + border-color: #218838; +} + +.tree-rename-actions { + display: inline-flex; + gap: 5px; + margin-left: 8px; +} + +.tree-rename-save-btn, .tree-rename-cancel-btn { + padding: 4px 8px; + border: none; + border-radius: 4px; + font-size: 0.75em; + cursor: pointer; + transition: background 0.2s; +} + +.tree-rename-save-btn { + background: #28a745; + color: white; +} + +.tree-rename-save-btn:hover { + background: #218838; +} + +.tree-rename-cancel-btn { + background: #6c757d; + color: white; +} + +.tree-rename-cancel-btn:hover { + background: #5a6268; +} + +/* Delete Confirmation Popup */ +.delete-confirm-popup { + position: fixed; + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 20px; + min-width: 320px; + max-width: 400px; + z-index: 10000; + border: 1px solid #e0e0e0; + animation: popupFadeIn 0.2s ease-out; + overflow: visible; +} + +.delete-confirm-popup.no-arrow::before, +.delete-confirm-popup.no-arrow::after { + display: none; +} + +@keyframes popupFadeIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +.delete-confirm-popup::before { + content: ''; + position: absolute; + bottom: -8px; + left: 20px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid white; +} + +.delete-confirm-popup::after { + content: ''; + position: absolute; + bottom: -9px; + left: 20px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid #e0e0e0; +} + +.delete-confirm-popup h3 { + margin: 0 0 12px 0; + color: #d32f2f; + font-size: 1.1em; + display: flex; + align-items: center; + gap: 8px; +} + +.delete-confirm-popup h3::before { + content: '⚠️'; + font-size: 1.2em; +} + +.delete-confirm-popup p { + margin: 0 0 16px 0; + color: #666; + line-height: 1.5; + font-size: 0.95em; +} + +.delete-confirm-popup .popup-buttons { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.delete-confirm-popup .popup-btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + font-size: 0.9em; + cursor: pointer; + transition: all 0.2s; + font-weight: 500; +} + +.delete-confirm-popup .popup-btn-cancel { + background: #f5f5f5; + color: #333; +} + +.delete-confirm-popup .popup-btn-cancel:hover { + background: #e0e0e0; +} + +.delete-confirm-popup .popup-btn-delete { + background: #d32f2f; + color: white; +} + +.delete-confirm-popup .popup-btn-delete:hover { + background: #b71c1c; +} + +.popup-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.1); + z-index: 9999; +} + diff --git a/resources/css/connections.css b/resources/css/connections.css new file mode 100644 index 0000000..9cb8139 --- /dev/null +++ b/resources/css/connections.css @@ -0,0 +1,27 @@ +/* Connections page specific styles */ + +.connection-status { + display: flex; + gap: 20px; + margin-top: 15px; +} + +.status-badge { + padding: 10px 20px; + border-radius: 6px; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.status-badge.success { + background: #d4edda; + color: #155724; +} + +.status-badge.error { + background: #f8d7da; + color: #721c24; +} + diff --git a/resources/css/migration.css b/resources/css/migration.css new file mode 100644 index 0000000..d20874f --- /dev/null +++ b/resources/css/migration.css @@ -0,0 +1,30 @@ +/* Migration page specific styles */ + +.store-mapping { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 15px; + align-items: center; + margin-top: 15px; +} + +.store-select { + padding: 12px; + border: 2px solid #ddd; + border-radius: 6px; + font-size: 1em; + width: 100%; + background: white; +} + +.store-select:focus { + outline: none; + border-color: #667eea; +} + +.arrow { + font-size: 1.5em; + color: #667eea; + text-align: center; +} + diff --git a/resources/css/products.css b/resources/css/products.css new file mode 100644 index 0000000..b1dd1ef --- /dev/null +++ b/resources/css/products.css @@ -0,0 +1,31 @@ +/* Products page specific styles */ +.products-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 6px; +} + +.products-table thead tr { + background: #f8f9fa; + border-bottom: 2px solid #dee2e6; +} + +.products-table th { + padding: 10px; + text-align: left; + font-weight: 600; + color: #333; + font-size: 0.9em; +} + +.products-table td { + padding: 8px 10px; + font-size: 0.85em; + border-bottom: 1px solid #e0e0e0; +} + +.products-table tbody tr:hover { + background: #f8f9fa; +} + diff --git a/resources/js/attributes.js b/resources/js/attributes.js new file mode 100644 index 0000000..a92537b --- /dev/null +++ b/resources/js/attributes.js @@ -0,0 +1,118 @@ +// Attributes page JavaScript + +let routes = {}; +let csrfToken = ''; + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + if (window.attributeRoutes) { + routes = window.attributeRoutes; + csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; + } +}); + +function migrateAttribute(attributeId, attributeCode) { + if (!confirm(`Are you sure you want to migrate the attribute "${attributeCode}" from Magento 1 to Magento 2?`)) { + return; + } + + const button = document.getElementById(`migrate-btn-${attributeId}`); + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Migrating...'; + + const url = routes.migrateAttribute.replace(':id', attributeId); + + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.getElementById(`attr-row-${attributeId}`); + if (row) { + row.style.opacity = '0.5'; + row.style.background = '#d4edda'; + button.textContent = 'Migrated ✓'; + button.style.background = '#28a745'; + button.disabled = true; + + const totalSpan = document.getElementById('missingAttributesTotal'); + if (totalSpan) { + const currentCount = parseInt(totalSpan.textContent); + if (currentCount > 0) { + totalSpan.textContent = currentCount - 1; + } + } + } + alert('Attribute migrated successfully!'); + } else { + button.disabled = false; + button.textContent = originalText; + alert('Error: ' + (data.message || 'Failed to migrate attribute')); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + alert('Error: ' + error.message); + }); +} + +function migrateAttributeGroup(groupId, setId, groupName) { + if (!confirm(`Are you sure you want to migrate the attribute group "${groupName}" from Magento 1 to Magento 2?\n\nThis will also migrate any attributes in this group that exist in Magento 2.`)) { + return; + } + + const button = document.getElementById(`migrate-group-btn-${groupId}-${setId}`); + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Migrating...'; + + const url = routes.migrateAttributeGroup + .replace(':groupId', groupId) + .replace(':setId', setId); + + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.getElementById(`group-row-${groupId}-${setId}`); + if (row) { + row.style.opacity = '0.5'; + row.style.background = '#d4edda'; + button.textContent = 'Migrated ✓'; + button.style.background = '#28a745'; + button.disabled = true; + } + const message = data.attributes_migrated !== undefined + ? `Attribute group migrated successfully! ${data.attributes_migrated} attribute(s) were also migrated to the group.` + : 'Attribute group migrated successfully!'; + alert(message); + } else { + button.disabled = false; + button.textContent = originalText; + alert('Error: ' + (data.message || 'Failed to migrate attribute group')); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + alert('Error: ' + error.message); + }); +} + +// Make functions available globally +window.migrateAttribute = migrateAttribute; +window.migrateAttributeGroup = migrateAttributeGroup; + diff --git a/resources/js/categories.js b/resources/js/categories.js new file mode 100644 index 0000000..22db55d --- /dev/null +++ b/resources/js/categories.js @@ -0,0 +1,268 @@ +// Categories page JavaScript + +let routes = {}; + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + if (window.categoryRoutes) { + routes = window.categoryRoutes; + loadCategoryTrees(); + } +}); + +function loadCategoryTrees() { + loadM1Tree(); + loadM2Tree(); +} + +function loadM1Tree() { + const container = document.getElementById('m1-tree-container'); + if (!container) return; + + container.innerHTML = '
Loading categories...
'; + + fetch(routes.magento1CategoryTree) + .then(response => response.json()) + .then(data => { + if (data.success) { + container.innerHTML = ''; + if (data.tree && data.tree.length > 0) { + renderTree(container, data.tree, 'm1'); + } else { + container.innerHTML = '
No categories found
'; + } + } else { + container.innerHTML = `
Error: ${data.message || 'Failed to load categories'}
`; + } + }) + .catch(error => { + container.innerHTML = `
Error: ${error.message}
`; + }); +} + +function loadM2Tree() { + const container = document.getElementById('m2-tree-container'); + if (!container) return; + + container.innerHTML = '
Loading categories...
'; + + fetch(routes.magento2CategoryTree) + .then(response => response.json()) + .then(data => { + if (data.success) { + container.innerHTML = ''; + if (data.tree && data.tree.length > 0) { + renderTree(container, data.tree, 'm2'); + } else { + container.innerHTML = '
No categories found
'; + } + } else { + container.innerHTML = `
Error: ${data.message || 'Failed to load categories'}
`; + } + }) + .catch(error => { + container.innerHTML = `
Error: ${error.message}
`; + }); +} + +function renderTree(container, nodes, source = 'm2') { + nodes.forEach(node => { + const nodeElement = createTreeNode(node, source); + container.appendChild(nodeElement); + }); +} + +function createTreeNode(node, source = 'm2') { + const nodeDiv = document.createElement('div'); + nodeDiv.className = 'tree-node'; + const hasChildren = node.children && node.children.length > 0; + const canDelete = node.id != 0 && node.id != 1; + + const itemDiv = document.createElement('div'); + itemDiv.className = 'tree-node-item'; + + const toggle = document.createElement('span'); + toggle.className = hasChildren ? 'tree-toggle collapsed' : 'tree-toggle leaf'; + if (hasChildren) { + toggle.onclick = function(e) { + e.stopPropagation(); + toggleNode(this); + }; + } + + const label = document.createElement('div'); + label.className = 'tree-label'; + + const labelText = document.createElement('span'); + labelText.className = 'tree-label-text'; + labelText.textContent = `[${node.id}] ${node.name || 'Unnamed Category'}`; + + const badge = document.createElement('span'); + badge.className = `tree-badge ${node.is_active ? 'active' : 'inactive'}`; + badge.textContent = node.is_active ? 'Active' : 'Inactive'; + + label.appendChild(labelText); + label.appendChild(badge); + + if (canDelete) { + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'tree-delete-btn'; + deleteBtn.textContent = 'Delete'; + deleteBtn.onclick = function(e) { + e.stopPropagation(); + showDeleteConfirmPopup(deleteBtn, node.id, node.name, source, hasChildren, node.children ? node.children.length : 0); + }; + label.appendChild(deleteBtn); + } + + itemDiv.appendChild(toggle); + itemDiv.appendChild(label); + nodeDiv.appendChild(itemDiv); + + if (hasChildren) { + const childrenDiv = document.createElement('div'); + childrenDiv.className = 'tree-children'; + node.children.forEach(child => { + childrenDiv.appendChild(createTreeNode(child, source)); + }); + nodeDiv.appendChild(childrenDiv); + } + + return nodeDiv; +} + +function toggleNode(toggleElement) { + const nodeItem = toggleElement.parentElement; + const nodeDiv = nodeItem.parentElement; + const childrenDivs = nodeDiv.querySelectorAll('.tree-children'); + + if (childrenDivs.length > 0) { + let isExpanded = false; + childrenDivs.forEach(div => { + if (div.style.display !== 'none') { + isExpanded = true; + } + }); + + if (isExpanded) { + childrenDivs.forEach(div => { + div.style.display = 'none'; + }); + toggleElement.classList.remove('expanded'); + toggleElement.classList.add('collapsed'); + } else { + childrenDivs.forEach(div => { + div.style.display = 'block'; + }); + toggleElement.classList.remove('collapsed'); + toggleElement.classList.add('expanded'); + } + } +} + +function showDeleteConfirmPopup(buttonElement, categoryId, categoryName, source, hasChildren, childrenCount) { + const existingPopup = document.querySelector('.delete-confirm-popup'); + const existingOverlay = document.querySelector('.popup-overlay'); + if (existingPopup) existingPopup.remove(); + if (existingOverlay) existingOverlay.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'popup-overlay'; + overlay.onclick = function() { + closeDeleteConfirmPopup(); + }; + document.body.appendChild(overlay); + + const popup = document.createElement('div'); + popup.className = 'delete-confirm-popup'; + popup.style.position = 'fixed'; + popup.style.top = '50%'; + popup.style.left = '50%'; + popup.style.transform = 'translate(-50%, -50%)'; + popup.style.zIndex = '10000'; + + const title = document.createElement('h3'); + title.textContent = 'Delete Category'; + + const message = document.createElement('p'); + if (hasChildren) { + message.innerHTML = `Are you sure you want to delete "${categoryName}" and all ${childrenCount} subcategory(ies)? This action cannot be undone.`; + } else { + message.innerHTML = `Are you sure you want to delete the category "${categoryName}"? This action cannot be undone.`; + } + + const buttonsDiv = document.createElement('div'); + buttonsDiv.className = 'popup-buttons'; + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'popup-btn popup-btn-cancel'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.onclick = function(e) { + e.stopPropagation(); + closeDeleteConfirmPopup(); + }; + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'popup-btn popup-btn-delete'; + deleteBtn.textContent = 'Delete'; + deleteBtn.onclick = function(e) { + e.stopPropagation(); + closeDeleteConfirmPopup(); + deleteCategory(categoryId, categoryName, source); + }; + + buttonsDiv.appendChild(cancelBtn); + buttonsDiv.appendChild(deleteBtn); + + popup.appendChild(title); + popup.appendChild(message); + popup.appendChild(buttonsDiv); + + document.body.appendChild(popup); +} + +function closeDeleteConfirmPopup() { + const popup = document.querySelector('.delete-confirm-popup'); + const overlay = document.querySelector('.popup-overlay'); + if (popup) popup.remove(); + if (overlay) overlay.remove(); +} + +function deleteCategory(categoryId, categoryName, source) { + const container = source === 'm1' ? document.getElementById('m1-tree-container') : document.getElementById('m2-tree-container'); + if (!container) return; + + const originalContent = container.innerHTML; + container.innerHTML = '
Deleting category...
'; + + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + const deleteRoute = routes.deleteCategory.replace(':id', categoryId); + + fetch(deleteRoute, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ source: source }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (source === 'm1') { + loadM1Tree(); + } else { + loadM2Tree(); + location.reload(); + } + } else { + container.innerHTML = originalContent; + alert('Error: ' + (data.message || 'Failed to delete category')); + } + }) + .catch(error => { + container.innerHTML = originalContent; + alert('Error: ' + error.message); + }); +} + diff --git a/resources/js/connections.js b/resources/js/connections.js new file mode 100644 index 0000000..696b9a7 --- /dev/null +++ b/resources/js/connections.js @@ -0,0 +1,28 @@ +// Connections page JavaScript +export function initConnections() { + const testRoute = document.querySelector('[data-test-route]')?.dataset.testRoute; + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + + window.testConnections = function() { + fetch(testRoute, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('✓ Both database connections are working!'); + location.reload(); + } else { + alert('✗ Connection test failed. Check the error messages above.'); + } + }) + .catch(error => { + alert('Error testing connections: ' + error.message); + }); + }; +} + diff --git a/resources/js/migration.js b/resources/js/migration.js new file mode 100644 index 0000000..a845c62 --- /dev/null +++ b/resources/js/migration.js @@ -0,0 +1,101 @@ +// Migration page JavaScript + +let routes = {}; +let csrfToken = ''; + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + if (window.migrationRoutes) { + routes = window.migrationRoutes; + csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; + } +}); + +function startMigration() { + const storeMapping = {}; + const selects = document.querySelectorAll('select[name^="store_mapping"]'); + + let hasMapping = false; + selects.forEach(select => { + if (select.value) { + storeMapping[select.name.match(/\[(\d+)\]/)[1]] = select.value; + hasMapping = true; + } + }); + + if (!hasMapping) { + alert('Please map at least one store before starting migration.'); + return; + } + + if (!confirm('Are you sure you want to start the migration? This will modify your Magento 2 database.')) { + return; + } + + document.getElementById('migrateBtn').disabled = true; + document.getElementById('loading').classList.add('active'); + + const migrationLogContent = document.getElementById('migrationLogContent'); + migrationLogContent.innerHTML = '
Starting migration...
'; + + document.getElementById('migrationLogContainer').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + + fetch(routes.migrate, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ store_mapping: storeMapping }) + }) + .then(response => response.json()) + .then(data => { + document.getElementById('loading').classList.remove('active'); + document.getElementById('migrateBtn').disabled = false; + + if (data.success) { + addLogEntry(`✓ Migration completed successfully!`, 'success'); + addLogEntry(`Total categories processed: ${data.migrated_count}`, 'success'); + if (data.added_count !== undefined) { + addLogEntry(` - Added new categories: ${data.added_count}`, 'success'); + } + if (data.existing_count !== undefined) { + addLogEntry(` - Found existing categories: ${data.existing_count}`, 'success'); + } + + if (data.log && data.log.length > 0) { + data.log.forEach(log => { + const type = log.includes('ERROR') ? 'error' : 'success'; + addLogEntry(log, type); + }); + } + } else { + addLogEntry(`✗ Migration failed: ${data.message}`, 'error'); + if (data.log && data.log.length > 0) { + data.log.forEach(log => { + addLogEntry(log, log.includes('ERROR') ? 'error' : 'success'); + }); + } + } + }) + .catch(error => { + document.getElementById('loading').classList.remove('active'); + document.getElementById('migrateBtn').disabled = false; + addLogEntry(`✗ Error: ${error.message}`, 'error'); + }); +} + +function addLogEntry(message, type = '') { + const migrationLogContent = document.getElementById('migrationLogContent'); + const entry = document.createElement('div'); + entry.className = `log-entry ${type}`; + entry.textContent = message; + migrationLogContent.appendChild(entry); + + const migrationLogContainer = document.getElementById('migrationLogContainer'); + migrationLogContainer.scrollTop = migrationLogContainer.scrollHeight; +} + +// Make startMigration available globally +window.startMigration = startMigration; + diff --git a/resources/js/products.js b/resources/js/products.js new file mode 100644 index 0000000..ae7f1a3 --- /dev/null +++ b/resources/js/products.js @@ -0,0 +1,358 @@ +// Products page JavaScript + +let routes = {}; +let csrfToken = ''; + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + if (window.productRoutes) { + routes = window.productRoutes; + csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; + } +}); + +function startProductMigration(dryRun) { + const button = dryRun ? document.getElementById('dryRunProductMigrationBtn') : document.getElementById('startProductMigrationBtn'); + const otherButton = dryRun ? document.getElementById('startProductMigrationBtn') : document.getElementById('dryRunProductMigrationBtn'); + const originalText = button.textContent; + button.disabled = true; + otherButton.disabled = true; + button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...'; + button.style.cursor = 'not-allowed'; + + const logContent = document.getElementById('productMigrationLogContent'); + logContent.innerHTML = '
' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '
'; + + fetch(routes.migrateProducts, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ dry_run: dryRun }) + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + otherButton.disabled = false; + button.textContent = originalText; + button.style.cursor = 'pointer'; + + if (data.success) { + const successEntry = document.createElement('div'); + successEntry.className = 'log-entry success'; + successEntry.textContent = `✓ ${dryRun ? 'Dry run' : 'Migration'} completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`; + logContent.appendChild(successEntry); + + if (data.log && data.log.length > 0) { + data.log.forEach(log => { + const entry = document.createElement('div'); + entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : 'success'); + entry.textContent = log; + logContent.appendChild(entry); + }); + } + + if (data.missing_attributes && data.missing_attributes.length > 0) { + const missingSection = document.getElementById('missingAttributesSection'); + const tbody = document.getElementById('missingAttributesTableBody'); + tbody.innerHTML = ''; + data.missing_attributes.forEach(attr => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${attr.attribute_code} + ${attr.frontend_label || 'N/A'} + ${attr.backend_type || 'N/A'} + `; + tbody.appendChild(row); + }); + missingSection.style.display = 'block'; + } + + const logContainer = document.getElementById('productMigrationLogContainer'); + logContainer.scrollTop = logContainer.scrollHeight; + } else { + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error'); + logContent.appendChild(errorEntry); + } + }) + .catch(error => { + button.disabled = false; + otherButton.disabled = false; + button.textContent = originalText; + button.style.cursor = 'pointer'; + + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ Error: ' + error.message; + logContent.appendChild(errorEntry); + }); +} + +function deleteM2Product(productId, productName, productSku) { + if (!confirm(`Are you sure you want to delete product "${productName}" (SKU: ${productSku})?`)) { + return; + } + + const url = routes.deleteProduct.replace(':id', productId); + + fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Product deleted successfully!'); + location.reload(); + } else { + alert('Error: ' + (data.message || 'Failed to delete product')); + } + }) + .catch(error => { + alert('Error: ' + error.message); + }); +} + +function deleteProductsAboveM1Max() { + if (!confirm('Are you sure you want to delete all Magento 2 products with entity_id greater than the maximum Magento 1 product ID? This action cannot be undone.')) { + return; + } + + const button = document.getElementById('deleteProductsAboveM1MaxBtn'); + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Deleting...'; + + fetch(routes.deleteProductsAboveM1Max, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + button.textContent = originalText; + + if (data.success) { + alert(`Successfully deleted ${data.deleted || 0} products.`); + location.reload(); + } else { + alert('Error: ' + (data.message || 'Failed to delete products')); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + alert('Error: ' + error.message); + }); +} + +function syncProductCategories() { + if (!confirm('Are you sure you want to sync product category assignments from Magento 1 to Magento 2?')) { + return; + } + + const button = document.getElementById('syncProductCategoriesBtn'); + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Syncing...'; + + fetch(routes.syncProductCategories, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + button.textContent = originalText; + + if (data.success) { + alert(`Sync completed! Updated: ${data.updated || 0}, Skipped: ${data.skipped || 0}, Errors: ${data.errors || 0}`); + } else { + alert('Error: ' + (data.message || 'Sync failed')); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + alert('Error: ' + error.message); + }); +} + +function loadM1CategoryTreeWithProducts() { + const container = document.getElementById('m1-category-products-tree-container'); + if (!container) return; + + container.innerHTML = '
Loading categories and products...
'; + + fetch(routes.magento1CategoryTreeWithProducts) + .then(response => response.json()) + .then(data => { + if (data.success) { + container.innerHTML = ''; + if (data.tree && data.tree.length > 0) { + renderCategoryTreeWithProducts(container, data.tree); + } else { + container.innerHTML = '
No categories found
'; + } + } else { + container.innerHTML = `
Error: ${data.message || 'Failed to load categories'}
`; + } + }) + .catch(error => { + container.innerHTML = `
Error: ${error.message}
`; + }); +} + +function loadM2CategoryTreeWithProducts() { + const container = document.getElementById('m2-category-products-tree-container'); + if (!container) return; + + container.innerHTML = '
Loading categories and products...
'; + + fetch(routes.magento2CategoryTreeWithProducts) + .then(response => response.json()) + .then(data => { + if (data.success) { + container.innerHTML = ''; + if (data.tree && data.tree.length > 0) { + renderCategoryTreeWithProducts(container, data.tree); + } else { + container.innerHTML = '
No categories found
'; + } + } else { + container.innerHTML = `
Error: ${data.message || 'Failed to load categories'}
`; + } + }) + .catch(error => { + container.innerHTML = `
Error: ${error.message}
`; + }); +} + +function renderCategoryTreeWithProducts(container, tree) { + tree.forEach(node => { + const nodeElement = createCategoryWithProductsNode(node); + container.appendChild(nodeElement); + }); +} + +function toggleNode(toggleElement) { + const nodeItem = toggleElement.parentElement; + const nodeDiv = nodeItem.parentElement; + const childrenDivs = nodeDiv.querySelectorAll('.tree-children'); + + if (childrenDivs.length > 0) { + let isExpanded = false; + childrenDivs.forEach(div => { + if (div.style.display !== 'none') { + isExpanded = true; + } + }); + + if (isExpanded) { + childrenDivs.forEach(div => { + div.style.display = 'none'; + }); + toggleElement.classList.remove('expanded'); + toggleElement.classList.add('collapsed'); + } else { + childrenDivs.forEach(div => { + div.style.display = 'block'; + }); + toggleElement.classList.remove('collapsed'); + toggleElement.classList.add('expanded'); + } + } +} + +function createCategoryWithProductsNode(node) { + const nodeDiv = document.createElement('div'); + nodeDiv.className = 'tree-node'; + const hasChildren = node.children && node.children.length > 0; + const hasProducts = node.products && node.products.length > 0; + const productCount = node.product_count !== undefined ? node.product_count : (node.products ? node.products.length : 0); + + const itemDiv = document.createElement('div'); + itemDiv.className = 'tree-node-item'; + + const toggle = document.createElement('span'); + toggle.className = (hasChildren || hasProducts) ? 'tree-toggle collapsed' : 'tree-toggle leaf'; + if (hasChildren || hasProducts) { + toggle.onclick = function(e) { + e.stopPropagation(); + toggleNode(this); + }; + } + + const label = document.createElement('div'); + label.className = 'tree-label'; + + const labelText = document.createElement('span'); + labelText.className = 'tree-label-text'; + labelText.textContent = `[${node.id}] ${node.name || 'Unnamed Category'} (${productCount} products)`; + + const badge = document.createElement('span'); + badge.className = `tree-badge ${node.is_active ? 'active' : 'inactive'}`; + badge.textContent = node.is_active ? 'Active' : 'Inactive'; + + label.appendChild(labelText); + label.appendChild(badge); + + itemDiv.appendChild(toggle); + itemDiv.appendChild(label); + nodeDiv.appendChild(itemDiv); + + // Add products section if products exist + if (hasProducts) { + const productsDiv = document.createElement('div'); + productsDiv.className = 'tree-children'; + productsDiv.style.display = 'none'; + + const productsHeader = document.createElement('div'); + productsHeader.style.cssText = 'padding: 8px 12px; font-weight: 600; color: #667eea; background: #f0f0f0; border-radius: 4px; margin: 5px 0;'; + productsHeader.textContent = `Products (${node.products.length}):`; + productsDiv.appendChild(productsHeader); + + node.products.forEach(product => { + const productDiv = document.createElement('div'); + productDiv.style.cssText = 'padding: 6px 12px 6px 30px; font-size: 0.9em; color: #666; border-left: 2px solid #e0e0e0; margin-left: 20px;'; + productDiv.textContent = `ID: ${product.id || product.product_id || 'N/A'} | SKU: ${product.sku || 'N/A'} | Name: ${product.name || 'Unnamed Product'}`; + productsDiv.appendChild(productDiv); + }); + + nodeDiv.appendChild(productsDiv); + } + + // Add children + if (hasChildren) { + const childrenDiv = document.createElement('div'); + childrenDiv.className = 'tree-children'; + childrenDiv.style.display = 'none'; + node.children.forEach(child => { + childrenDiv.appendChild(createCategoryWithProductsNode(child)); + }); + nodeDiv.appendChild(childrenDiv); + } + + return nodeDiv; +} + +// Make functions available globally +window.startProductMigration = startProductMigration; +window.deleteM2Product = deleteM2Product; +window.deleteProductsAboveM1Max = deleteProductsAboveM1Max; +window.syncProductCategories = syncProductCategories; +window.loadM1CategoryTreeWithProducts = loadM1CategoryTreeWithProducts; +window.loadM2CategoryTreeWithProducts = loadM2CategoryTreeWithProducts; + diff --git a/resources/views/attributes/index.blade.php b/resources/views/attributes/index.blade.php new file mode 100644 index 0000000..1c06d41 --- /dev/null +++ b/resources/views/attributes/index.blade.php @@ -0,0 +1,274 @@ +@extends('layouts.app') + +@section('content') + +
+

📦 Attribute Groups

+

View attribute groups that organize attributes within attribute sets:

+ +
+ +
+

Magento 1 Attribute Groups ({{ $m1AttributeGroups->count() }})

+
+ @if($m1AttributeGroups->count() > 0) + + + + + + + + + + + + + @foreach($m1AttributeGroups as $group) + @php + $groupKey = ($group->attribute_set_name ?? 'Default') . '|' . ($group->attribute_group_name ?? ''); + $isMissing = $m1AttributeGroupsMissingInM2->contains(function($missingGroup) use ($groupKey) { + return ($missingGroup->attribute_set_name ?? 'Default') . '|' . ($missingGroup->attribute_group_name ?? '') === $groupKey; + }); + @endphp + + + + + + + + + @endforeach + +
Group IDGroup NameAttribute SetAttributesSort OrderActions
{{ $group->attribute_group_id }}{{ $group->attribute_group_name ?? 'N/A' }}{{ $group->attribute_set_name ?? 'Default' }}{{ $group->attribute_count ?? 0 }}{{ $group->sort_order ?? 0 }} + @if($isMissing) + + @else + ✓ Exists + @endif +
+ @else +
+ No attribute groups found. +
+ @endif +
+
+ + +
+

Magento 2 Attribute Groups ({{ $m2AttributeGroups->count() }})

+
+ @if($m2AttributeGroups->count() > 0) + + + + + + + + + + + + @foreach($m2AttributeGroups as $group) + + + + + + + + @endforeach + +
Group IDGroup NameAttribute SetAttributesSort Order
{{ $group->attribute_group_id }}{{ $group->attribute_group_name ?? 'N/A' }}{{ $group->attribute_set_name ?? 'Default' }}{{ $group->attribute_count ?? 0 }}{{ $group->sort_order ?? 0 }}
+ @else +
+ No attribute groups found. +
+ @endif +
+
+
+
+ + +
+

⚠️ Missing Attributes

+

These attributes exist in Magento 1 but are missing in Magento 2:

+ + @if($m1AttributesMissingInM2->count() > 0) +
+ + + + + + + + + + + + + + + @foreach($m1AttributesMissingInM2 as $attr) + + + + + + + + + + + @endforeach + +
IDCodeLabelTypeInputRequiredUser DefinedActions
{{ $attr->attribute_id }}{{ $attr->attribute_code }}{{ $attr->frontend_label ?? 'N/A' }}{{ $attr->backend_type ?? 'N/A' }}{{ $attr->frontend_input ?? 'N/A' }} + @if($attr->is_required ?? 0) + Yes + @else + No + @endif + + @if($attr->is_user_defined ?? 0) + Yes + @else + No + @endif + + +
+
+
+ Total: {{ $m1AttributesMissingInM2->count() }} {{ Str::plural('attribute', $m1AttributesMissingInM2->count()) }} found in Magento 1 but not in Magento 2. +
+ @else +
+ ✓ All Magento 1 attributes have matching attribute codes in Magento 2. +
+ @endif +
+ + +
+

📋 Category Attributes

+

View all category attributes from Magento 1 and Magento 2

+ +
+ +
+

Magento 1 Attributes ({{ $m1Attributes->count() }})

+
+ @if($m1Attributes->count() > 0) + + + + + + + + + + + + + @foreach($m1Attributes as $attr) + + + + + + + + + @endforeach + +
IDCodeLabelTypeInputRequired
{{ $attr->attribute_id }}{{ $attr->attribute_code }}{{ $attr->frontend_label ?? 'N/A' }}{{ $attr->backend_type ?? 'N/A' }}{{ $attr->frontend_input ?? 'N/A' }} + @if($attr->is_required ?? 0) + Yes + @else + No + @endif +
+ @else +
+ No attributes found. +
+ @endif +
+
+ + +
+

Magento 2 Attributes ({{ $m2Attributes->count() }})

+
+ @if($m2Attributes->count() > 0) + + + + + + + + + + + + + @foreach($m2Attributes as $attr) + + + + + + + + + @endforeach + +
IDCodeLabelTypeInputRequired
{{ $attr->attribute_id }}{{ $attr->attribute_code }}{{ $attr->frontend_label ?? 'N/A' }}{{ $attr->backend_type ?? 'N/A' }}{{ $attr->frontend_input ?? 'N/A' }} + @if($attr->is_required ?? 0) + Yes + @else + No + @endif +
+ @else +
+ No attributes found. +
+ @endif +
+
+
+
+@endsection + +{{-- CSS is loaded globally via app.css --}} + +@push('scripts') + @vite(['resources/js/attributes.js']) + +@endpush + diff --git a/resources/views/categories/index.blade.php b/resources/views/categories/index.blade.php new file mode 100644 index 0000000..c481f92 --- /dev/null +++ b/resources/views/categories/index.blade.php @@ -0,0 +1,123 @@ +@extends('layouts.app') + +@section('content') + +
+

📊 Category Statistics

+
+
+
{{ $m1CategoriesCount }}
+
Magento 1 Categories
+
+
+
{{ $m2CategoriesCount }}
+
Magento 2 Categories
+
+
+
{{ $m1CategoriesCount - $m2CategoriesCount }}
+
Difference
+
+
+
+ + +
+

⚠️ Magento 2 Categories Not Found in Magento 1

+

These categories exist in Magento 2 but do not have a matching name in Magento 1:

+ + @if($m2CategoriesNotInM1->count() > 0) +
+ + + + + + + + + + + + + + @foreach($m2CategoriesNotInM1 as $category) + + + + + + + + + + @endforeach + +
IDCategory NameLevelStatusRoot CategoryPathActions
{{ $category->entity_id }}{{ $category->name ?? 'Unnamed Category' }}{{ $category->level ?? 'N/A' }} + + {{ ($category->is_active ?? 0) ? 'Active' : 'Inactive' }} + + + @if(isset($category->root_category_name) && $category->root_category_name !== 'N/A') + {{ $category->root_category_name }} + @if(isset($category->root_category_id)) + (ID: {{ $category->root_category_id }}) + @endif + @else + N/A + @endif + {{ $category->path ?? 'N/A' }} + +
+
+
+ Total: {{ $m2CategoriesNotInM1->count() }} {{ Str::plural('category', $m2CategoriesNotInM1->count()) }} found in Magento 2 but not in Magento 1. +
+ @else +
+ ✓ All Magento 2 categories have matching names in Magento 1. +
+ @endif +
+ +
+

🌳 Category Trees

+

View category hierarchies from Magento 1 and Magento 2

+ +
+ +
+

Magento 1 Categories

+
+
Loading categories...
+
+
+ + +
+

Magento 2 Categories

+
+
Loading categories...
+
+
+
+
+@endsection + +{{-- CSS is loaded globally via app.css --}} + +@push('scripts') + @vite(['resources/js/categories.js']) + +@endpush + diff --git a/resources/views/connections/index.blade.php b/resources/views/connections/index.blade.php new file mode 100644 index 0000000..acd91a7 --- /dev/null +++ b/resources/views/connections/index.blade.php @@ -0,0 +1,40 @@ +@extends('layouts.app') + +@section('content') +
+

📡 Database Connections

+
+
+ Magento 1: + + {{ $connectionTest['magento1'] ? '✓ Connected' : '✗ Failed' }} + + @if(!$connectionTest['magento1'] && isset($connectionTest['magento1_error'])) +
+ {{ $connectionTest['magento1_error'] }} +
+ @endif +
+
+ Magento 2: + + {{ $connectionTest['magento2'] ? '✓ Connected' : '✗ Failed' }} + + @if(!$connectionTest['magento2'] && isset($connectionTest['magento2_error'])) +
+ {{ $connectionTest['magento2_error'] }} +
+ @endif +
+
+ +
+@endsection + +{{-- CSS is loaded globally via app.css --}} + +@push('scripts') + @vite(['resources/js/connections.js']) +
+@endpush + diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..e97b828 --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,28 @@ + + + + + + + Magento Migration Tool + @vite(['resources/css/app.css']) + @stack('styles') + + +
+
+

🛒 Magento Migration

+

Migrate categories from Magento 1 to Magento 2 with multi-store support

+
+ +
+ @include('partials.navigation') + + @yield('content') +
+
+ + @stack('scripts') + + + diff --git a/resources/views/migration/index.blade.php b/resources/views/migration/index.blade.php index e475d95..8d926b2 100644 --- a/resources/views/migration/index.blade.php +++ b/resources/views/migration/index.blade.php @@ -1,2764 +1,114 @@ - - - - - - Magento Migration Tool - - - -
-
-

🛒 Magento Migration

-

Migrate categories from Magento 1 to Magento 2 with multi-store support

-
- -
- -
- - - - - +@extends('layouts.app') + +@section('content') + +
+

📊 Statistics

+
+
+
{{ $m1Stores->count() }}
+
Magento 1 Stores
- - -
-
-

📡 Database Connections

-
-
- Magento 1: - - {{ $connectionTest['magento1'] ? '✓ Connected' : '✗ Failed' }} - - @if(!$connectionTest['magento1'] && isset($connectionTest['magento1_error'])) -
- {{ $connectionTest['magento1_error'] }} -
- @endif -
-
- Magento 2: - - {{ $connectionTest['magento2'] ? '✓ Connected' : '✗ Failed' }} - - @if(!$connectionTest['magento2'] && isset($connectionTest['magento2_error'])) -
- {{ $connectionTest['magento2_error'] }} -
- @endif -
-
- -
+
+
{{ $m2Stores->count() }}
+
Magento 2 Stores
- - -
- -
-

📊 Statistics

-
-
-
{{ $m1Stores->count() }}
-
Magento 1 Stores
-
-
-
{{ $m2Stores->count() }}
-
Magento 2 Stores
-
-
-
{{ $m1CategoriesCount }}
-
Magento 1 Categories
-
-
-
{{ $m2CategoriesCount }}
-
Magento 2 Categories
-
-
+
+
{{ $m1CategoriesCount }}
+
Magento 1 Categories
- - -
-

🔄 Store Mapping

-

Map each Magento 1 store to its corresponding Magento 2 store:

- -
- @foreach($m1Stores as $m1Store) -
-
- M1 Store: {{ $m1Store->name }} ({{ $m1Store->code }}) -
-
-
- -
-
- @endforeach -
- - @if($m1Stores->isEmpty()) -
-

No Magento 1 stores found. Please check your database connection.

-
- @endif -
- - -
-

⚡ Actions

- -
-

What happens when you click "Start Migration"?

-

The migration process will:

-
    -
  • Migrate category structure: Create categories in Magento 2 based on the hierarchy from Magento 1, preserving parent-child relationships and positions
  • -
  • Migrate category attributes: For each mapped store, migrate category names, URL keys, and active status from Magento 1 to Magento 2
  • -
  • Preserve hierarchy: Maintain the exact category tree structure with proper levels and paths
  • -
  • Handle existing categories: If a category with the same name and parent already exists in Magento 2, it will be reused instead of creating a duplicate
  • -
  • Generate logs: Provide detailed migration logs showing which categories were added, updated, or encountered errors
  • -
-

⚠️ Warning: This will modify your Magento 2 database. Make sure you have a backup before proceeding.

-
- -
- - -
- -
-
-

Migration in progress...

-
-
- - -
-

📋 Migration Logs

-

Detailed logs showing which categories were added, updated, or encountered errors during migration:

- -
-
-
- No migration logs yet. Click "Start Migration" to begin. -
-
-
-
-
- - -
- -
-

📊 Category Statistics

-
-
-
{{ $m1CategoriesCount }}
-
Magento 1 Categories
-
-
-
{{ $m2CategoriesCount }}
-
Magento 2 Categories
-
-
-
{{ $m1CategoriesCount - $m2CategoriesCount }}
-
Difference
-
-
-
- - -
-

⚠️ Magento 2 Categories Not Found in Magento 1

-

These categories exist in Magento 2 but do not have a matching name in Magento 1:

- - @if($m2CategoriesNotInM1->count() > 0) -
- - - - - - - - - - - - - - @foreach($m2CategoriesNotInM1 as $category) - - - - - - - - - - @endforeach - -
IDCategory NameLevelStatusRoot CategoryPathActions
{{ $category->entity_id }}{{ $category->name ?? 'Unnamed Category' }}{{ $category->level ?? 'N/A' }} - - {{ ($category->is_active ?? 0) ? 'Active' : 'Inactive' }} - - - @if(isset($category->root_category_name) && $category->root_category_name !== 'N/A') - {{ $category->root_category_name }} - @if(isset($category->root_category_id)) - (ID: {{ $category->root_category_id }}) - @endif - @else - N/A - @endif - {{ $category->path ?? 'N/A' }} - -
-
-
- Total: {{ $m2CategoriesNotInM1->count() }} {{ Str::plural('category', $m2CategoriesNotInM1->count()) }} found in Magento 2 but not in Magento 1. -
- @else -
- ✓ All Magento 2 categories have matching names in Magento 1. -
- @endif -
- -
-

🌳 Category Trees

-

View category hierarchies from Magento 1 and Magento 2

- -
- -
-

Magento 1 Categories

-
-
Loading categories...
-
-
- - -
-

Magento 2 Categories

-
-
Loading categories...
-
-
-
-
-
- - -
- -
-

📦 Attribute Groups

-

View attribute groups that organize attributes within attribute sets:

- -
- -
-

Magento 1 Attribute Groups ({{ $m1AttributeGroups->count() }})

-
- @if($m1AttributeGroups->count() > 0) - - - - - - - - - - - - - @foreach($m1AttributeGroups as $group) - @php - $groupKey = ($group->attribute_set_name ?? 'Default') . '|' . ($group->attribute_group_name ?? ''); - $isMissing = $m1AttributeGroupsMissingInM2->contains(function($missingGroup) use ($groupKey) { - $missingKey = ($missingGroup->attribute_set_name ?? 'Default') . '|' . ($missingGroup->attribute_group_name ?? ''); - return $missingKey === $groupKey; - }); - @endphp - - - - - - - - - @endforeach - -
Group IDGroup NameAttribute SetAttributesSort OrderActions
{{ $group->attribute_group_id }}{{ $group->attribute_group_name ?? 'N/A' }}{{ $group->attribute_set_name ?? 'N/A' }} - {{ $group->attribute_count ?? 0 }} - {{ $group->sort_order ?? 'N/A' }} - @if($isMissing) - - @else - ✓ Exists - @endif -
- @else -
- No attribute groups found. Please check your database connection. -
- @endif -
-
- - -
-

Magento 2 Attribute Groups ({{ $m2AttributeGroups->count() }})

-
- @if($m2AttributeGroups->count() > 0) - - - - - - - - - - - - @foreach($m2AttributeGroups as $group) - - - - - - - - @endforeach - -
Group IDGroup NameAttribute SetAttributesSort Order
{{ $group->attribute_group_id }}{{ $group->attribute_group_name ?? 'N/A' }}{{ $group->attribute_set_name ?? 'N/A' }} - {{ $group->attribute_count ?? 0 }} - {{ $group->sort_order ?? 'N/A' }}
- @else -
- No attribute groups found. Please check your database connection. -
- @endif -
-
-
-
- - -
-

⚠️ Magento 1 Attributes Missing in Magento 2

-

These attributes exist in Magento 1 but do not have a matching attribute code in Magento 2:

- @if($m1AttributesMissingInM2->count() > 0) -
- - - - - - - - - - - - - - - @foreach($m1AttributesMissingInM2 as $attr) - - - - - - - - - - - @endforeach - -
IDAttribute CodeLabelTypeInputRequiredUser DefinedActions
{{ $attr->attribute_id }}{{ $attr->attribute_code }}{{ $attr->frontend_label ?? 'N/A' }}{{ $attr->backend_type ?? 'N/A' }}{{ $attr->frontend_input ?? 'N/A' }} - @if($attr->is_required ?? 0) - Yes - @else - No - @endif - - @if($attr->is_user_defined ?? 0) - Yes - @else - No - @endif - - -
-
-
- Total: {{ $m1AttributesMissingInM2->count() }} {{ Str::plural('attribute', $m1AttributesMissingInM2->count()) }} found in Magento 1 but not in Magento 2. -
- @else -
- ✓ All Magento 1 attributes have matching attribute codes in Magento 2. -
- @endif -
- - -
-

📋 Category Attributes

-

View all category attributes from Magento 1 and Magento 2

- -
- -
-

Magento 1 Attributes ({{ $m1Attributes->count() }})

-
- @if($m1Attributes->count() > 0) - - - - - - - - - - - - - @foreach($m1Attributes as $attr) - - - - - - - - - @endforeach - -
IDCodeLabelTypeInputRequired
{{ $attr->attribute_id }}{{ $attr->attribute_code }}{{ $attr->frontend_label ?? 'N/A' }}{{ $attr->backend_type ?? 'N/A' }}{{ $attr->frontend_input ?? 'N/A' }} - @if($attr->is_required ?? 0) - Yes - @else - No - @endif -
- @else -
- No attributes found. Please check your database connection. -
- @endif -
-
- - -
-

Magento 2 Attributes ({{ $m2Attributes->count() }})

-
- @if($m2Attributes->count() > 0) - - - - - - - - - - - - - @foreach($m2Attributes as $attr) - - - - - - - - - @endforeach - -
IDCodeLabelTypeInputRequired
{{ $attr->attribute_id }}{{ $attr->attribute_code }}{{ $attr->frontend_label ?? 'N/A' }}{{ $attr->backend_type ?? 'N/A' }}{{ $attr->frontend_input ?? 'N/A' }} - @if($attr->is_required ?? 0) - Yes - @else - No - @endif -
- @else -
- No attributes found. Please check your database connection. -
- @endif -
-
-
-
-
- - -
- -
-

🚀 Product Migration

-
-

What happens when you click "Start Product Migration"?

-

The product migration process will:

-
    -
  • Create new products: If a product with the same SKU doesn't exist in Magento 2, it will be created with all its attributes (name, description, price, weight, status, etc.)
  • -
  • Update existing products: If a product with the same SKU already exists in Magento 2, it will be updated with the latest data from Magento 1
  • -
  • Migrate product attributes: All product attributes including SKU, name, description, price, weight, status, visibility, and tax class will be migrated
  • -
  • Assign to categories: Products will be assigned to the corresponding categories in Magento 2 based on the category mapping from previous migrations
  • -
  • Preserve product types: Product types (simple, configurable, etc.) will be maintained
  • -
  • Generate logs: Detailed migration logs showing which products were added, updated, or encountered errors
  • -
-

⚠️ Warning: This will modify your Magento 2 database. Make sure you have a backup before proceeding.

-
- -
- - - -
-
- - - - - -
-

📋 Product Migration Logs

-

Detailed logs showing which products were added, updated, or encountered errors during migration:

-
-
-
- No product migration logs yet. Click "Run Dry Run" or "Start Product Migration" to begin. -
-
-
-
- - -
-

🌳 Category Tree with Products

-

View all categories with their associated products in a tree structure.

- -
- -
-

Magento 1 Categories

-
-
Click "Load M1 Category Tree" to view categories and their products.
-
-
- -
-
- - -
-

Magento 2 Categories

-
-
Click "Load M2 Category Tree" to view categories and their products.
-
-
- -
-
-
-
- - -
-

🔄 Sync Product Categories

-
-

This tool will update Magento 2 products to be in the same categories as their Magento 1 counterparts.

-
    -
  • Matches products between M1 and M2 by SKU or Product ID
  • -
  • Uses the category mapping from previous migrations
  • -
  • Adds missing category associations
  • -
  • Removes category associations that don't exist in M1
  • -
  • Preserves product positions in categories
  • -
-

⚠️ Warning: This will modify product-category associations in your Magento 2 database. Make sure you have a backup before proceeding.

-
- -
- -
- - -
-

Sync Logs

- -
-
- - -
-

⚠️ Magento 1 Products Not Found in Magento 2

-

These products exist in Magento 1 but do not have a matching SKU or ID in Magento 2:

- @if($m1ProductsNotInM2->count() > 0) -
- - - - - - - - - - - - @foreach($m1ProductsNotInM2 as $product) - - - - - - - - @endforeach - -
IDSKUNameTypeCreated
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} - @if($product->created_at) - {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} - @else - N/A - @endif -
-
-
- Total: {{ $m1ProductsNotInM2->count() }} {{ Str::plural('product', $m1ProductsNotInM2->count()) }} found in Magento 1 but not in Magento 2. -
- @else -
- ✓ All Magento 1 products have matching SKUs or IDs in Magento 2. -
- @endif -
- - -
-

⚠️ Magento 2 Products Not Found in Magento 1

-

These products exist in Magento 2 but do not have a matching SKU or ID in Magento 1:

- @if($m2ProductsNotInM1->count() > 0) -
- - - - - - - - - - - - - @foreach($m2ProductsNotInM1 as $product) - - - - - - - - - @endforeach - -
IDSKUNameTypeCreatedActions
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} - @if($product->created_at) - {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} - @else - N/A - @endif - - -
-
-
- Total: {{ $m2ProductsNotInM1->count() }} {{ Str::plural('product', $m2ProductsNotInM1->count()) }} found in Magento 2 but not in Magento 1. -
- @else -
- ✓ All Magento 2 products have matching SKUs or IDs in Magento 1. -
- @endif -
- - -
-

📦 Products

-

View all products from Magento 1 and Magento 2

- -
- -
-

Magento 1 Products ({{ $m1Products->count() }})

-
- @if($m1Products->count() > 0) - - - - - - - - - - - - @foreach($m1Products as $product) - - - - - - - - @endforeach - -
IDSKUNameTypeCreated
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} - @if($product->created_at) - {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} - @else - N/A - @endif -
- @else -
- No products found. Please check your database connection. -
- @endif -
-
- - -
-

Magento 2 Products ({{ $m2Products->count() }})

-
- @if($m2Products->count() > 0) - - - - - - - - - - - - @foreach($m2Products as $product) - - - - - - - - @endforeach - -
IDSKUNameTypeCreated
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} - @if($product->created_at) - {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} - @else - N/A - @endif -
- @else -
- No products found. Please check your database connection. -
- @endif -
-
-
-
+
+
{{ $m2CategoriesCount }}
+
Magento 2 Categories
+ +
+

🔄 Store Mapping

+

Map each Magento 1 store to its corresponding Magento 2 store:

+ +
+ @foreach($m1Stores as $m1Store) +
+
+ M1 Store: {{ $m1Store->name }} ({{ $m1Store->code }}) +
+
+
+ +
+
+ @endforeach +
+ + @if($m1Stores->isEmpty()) +
+

No Magento 1 stores found. Please check your database connection.

+
+ @endif +
+ + +
+

⚡ Actions

+ +
+

What happens when you click "Start Migration"?

+

The migration process will:

+
    +
  • Migrate category structure: Create categories in Magento 2 based on the hierarchy from Magento 1, preserving parent-child relationships and positions
  • +
  • Migrate category attributes: For each mapped store, migrate category names, URL keys, and active status from Magento 1 to Magento 2
  • +
  • Preserve hierarchy: Maintain the exact category tree structure with proper levels and paths
  • +
  • Handle existing categories: If a category with the same name and parent already exists in Magento 2, it will be reused instead of creating a duplicate
  • +
  • Generate logs: Provide detailed migration logs showing which categories were added, updated, or encountered errors
  • +
+

⚠️ Warning: This will modify your Magento 2 database. Make sure you have a backup before proceeding.

+
+ +
+ + +
+ +
+
+

Migration in progress...

+
+
+ + +
+

📋 Migration Logs

+

Detailed logs showing which categories were added, updated, or encountered errors during migration:

+ +
+
+
+ No migration logs yet. Click "Start Migration" to begin. +
+
+
+
+@endsection + +{{-- CSS is loaded globally via app.css --}} + +@push('scripts') + @vite(['resources/js/migration.js']) - - - +@endpush diff --git a/resources/views/partials/navigation.blade.php b/resources/views/partials/navigation.blade.php new file mode 100644 index 0000000..298ee21 --- /dev/null +++ b/resources/views/partials/navigation.blade.php @@ -0,0 +1,18 @@ + + diff --git a/resources/views/products/index.blade.php b/resources/views/products/index.blade.php new file mode 100644 index 0000000..02ef86c --- /dev/null +++ b/resources/views/products/index.blade.php @@ -0,0 +1,234 @@ +@extends('layouts.app') + +@section('content') + +
+

🚀 Product Migration

+
+

What happens when you click "Start Product Migration"?

+

The product migration process will:

+
    +
  • Create new products: If a product with the same SKU doesn't exist in Magento 2, it will be created with all its attributes
  • +
  • Update existing products: If a product with the same SKU already exists in Magento 2, it will be updated with the latest data from Magento 1
  • +
  • Migrate product attributes: All product attributes including SKU, name, description, price, weight, status, visibility, and tax class will be migrated
  • +
  • Assign to categories: Products will be assigned to the corresponding categories in Magento 2
  • +
  • Generate logs: Detailed migration logs showing which products were added, updated, or encountered errors
  • +
+

⚠️ Warning: This will modify your Magento 2 database. Make sure you have a backup before proceeding.

+
+ +
+ + + +
+
+ + + + + +
+

📋 Product Migration Logs

+

Detailed logs showing which products were added, updated, or encountered errors during migration:

+
+
+
+ No product migration logs yet. Click "Run Dry Run" or "Start Product Migration" to begin. +
+
+
+
+ + +
+

📊 Product Statistics

+
+
+
{{ $m1Products->count() }}
+
Magento 1 Products
+
+
+
{{ $m2Products->count() }}
+
Magento 2 Products
+
+
+
{{ $m1ProductsNotInM2->count() }}
+
M1 Products Not in M2
+
+
+
{{ $m2ProductsNotInM1->count() }}
+
M2 Products Not in M1
+
+
+
+ + + @if($m1ProductsNotInM2->count() > 0) +
+

⚠️ Magento 1 Products Not in Magento 2

+

These products exist in Magento 1 but are missing in Magento 2:

+
+ + + + + + + + + + + + @foreach($m1ProductsNotInM2->take(50) as $product) + + + + + + + + @endforeach + +
IDSKUNameTypeStatus
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} + @if(($product->status ?? 0) == 1) + Enabled + @else + Disabled + @endif +
+ @if($m1ProductsNotInM2->count() > 50) +

Showing first 50 of {{ $m1ProductsNotInM2->count() }} products.

+ @endif +
+
+ @endif + + + @if($m2ProductsNotInM1->count() > 0) +
+

⚠️ Magento 2 Products Not in Magento 1

+

These products exist in Magento 2 but are missing in Magento 1:

+
+ + + + + + + + + + + + + @foreach($m2ProductsNotInM1->take(50) as $product) + + + + + + + + + @endforeach + +
IDSKUNameTypeStatusActions
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} + @if(($product->status ?? 0) == 1) + Enabled + @else + Disabled + @endif + + +
+ @if($m2ProductsNotInM1->count() > 50) +

Showing first 50 of {{ $m2ProductsNotInM1->count() }} products.

+ @endif +
+
+ @endif + + +
+

🔄 Sync Product Categories

+

Sync product category assignments from Magento 1 to Magento 2:

+ +
+ + +
+

🌳 Category Tree with Products

+

View all categories with their associated products in a tree structure

+ +
+ +
+

Magento 1 Categories

+ +
+
Click the button above to load categories with products
+
+
+ + +
+

Magento 2 Categories

+ +
+
Click the button above to load categories with products
+
+
+
+
+@endsection + +{{-- CSS is loaded globally via app.css --}} + +@push('scripts') + @vite(['resources/js/products.js']) + +@endpush + diff --git a/routes/web.php b/routes/web.php index ecbc5a9..beb73c4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,28 +1,53 @@ group(function () { - Route::get('/', [MagentoMigrationController::class, 'index'])->name('migration.index'); - Route::post('/migrate', [MagentoMigrationController::class, 'migrate'])->name('migration.migrate'); - Route::get('/test-connections', [MagentoMigrationController::class, 'testConnections'])->name('migration.test-connections'); - Route::get('/magento1-categories', [MagentoMigrationController::class, 'getMagento1Categories'])->name('migration.magento1-categories'); - Route::get('/magento1-category-tree', [MagentoMigrationController::class, 'getMagento1CategoryTree'])->name('migration.magento1-category-tree'); - Route::get('/magento1-category-tree-with-products', [MagentoMigrationController::class, 'getMagento1CategoryTreeWithProducts'])->name('migration.magento1-category-tree-with-products'); - Route::get('/magento2-category-tree', [MagentoMigrationController::class, 'getMagento2CategoryTree'])->name('migration.magento2-category-tree'); - Route::get('/magento2-category-tree-with-products', [MagentoMigrationController::class, 'getMagento2CategoryTreeWithProducts'])->name('migration.magento2-category-tree-with-products'); - Route::get('/m2-categories-not-in-m1', [MagentoMigrationController::class, 'getM2CategoriesNotInM1'])->name('migration.m2-categories-not-in-m1'); - Route::delete('/category/{categoryId}', [MagentoMigrationController::class, 'deleteCategory'])->name('migration.delete-category'); - Route::put('/category/{categoryId}/rename', [MagentoMigrationController::class, 'renameCategory'])->name('migration.rename-category'); - Route::post('/attribute/{attributeId}/migrate', [MagentoMigrationController::class, 'migrateAttribute'])->name('migration.migrate-attribute'); - Route::post('/attribute-group/{groupId}/{setId}/migrate', [MagentoMigrationController::class, 'migrateAttributeGroup'])->name('migration.migrate-attribute-group'); - Route::post('/products/migrate', [MagentoMigrationController::class, 'migrateProducts'])->name('migration.migrate-products'); - Route::post('/products/sync-categories', [MagentoMigrationController::class, 'syncProductCategories'])->name('migration.sync-product-categories'); - Route::delete('/products/{productId}', [MagentoMigrationController::class, 'deleteM2Product'])->name('migration.delete-product'); - Route::delete('/products/above-m1-max', [MagentoMigrationController::class, 'deleteM2ProductsAboveM1Max'])->name('migration.delete-products-above-m1-max'); +// Connections routes +Route::prefix('connections')->name('connections.')->group(function () { + Route::get('/', [ConnectionsController::class, 'index'])->name('index'); + Route::get('/test', [ConnectionsController::class, 'testConnections'])->name('test-connections'); +}); + +// Categories routes +Route::prefix('categories')->name('categories.')->group(function () { + Route::get('/', [CategoriesController::class, 'index'])->name('index'); + Route::get('/magento1', [CategoriesController::class, 'getMagento1Categories'])->name('magento1-categories'); + Route::get('/magento1-tree', [CategoriesController::class, 'getMagento1CategoryTree'])->name('magento1-category-tree'); + Route::get('/magento1-tree-with-products', [CategoriesController::class, 'getMagento1CategoryTreeWithProducts'])->name('magento1-category-tree-with-products'); + Route::get('/magento2-tree', [CategoriesController::class, 'getMagento2CategoryTree'])->name('magento2-category-tree'); + Route::get('/magento2-tree-with-products', [CategoriesController::class, 'getMagento2CategoryTreeWithProducts'])->name('magento2-category-tree-with-products'); + Route::get('/m2-not-in-m1', [CategoriesController::class, 'getM2CategoriesNotInM1'])->name('m2-categories-not-in-m1'); + Route::delete('/{categoryId}', [CategoriesController::class, 'deleteCategory'])->name('delete-category'); + Route::put('/{categoryId}/rename', [CategoriesController::class, 'renameCategory'])->name('rename-category'); +}); + +// Migration routes +Route::prefix('migration')->name('migration.')->group(function () { + Route::get('/', [MigrationController::class, 'index'])->name('index'); + Route::post('/migrate', [MigrationController::class, 'migrate'])->name('migrate'); +}); + +// Attributes routes +Route::prefix('attributes')->name('attributes.')->group(function () { + Route::get('/', [AttributesController::class, 'index'])->name('index'); + Route::post('/{attributeId}/migrate', [AttributesController::class, 'migrateAttribute'])->name('migrate-attribute'); + Route::post('/group/{groupId}/{setId}/migrate', [AttributesController::class, 'migrateAttributeGroup'])->name('migrate-attribute-group'); +}); + +// Products routes +Route::prefix('products')->name('products.')->group(function () { + Route::get('/', [ProductsController::class, 'index'])->name('index'); + Route::post('/migrate', [ProductsController::class, 'migrateProducts'])->name('migrate-products'); + Route::post('/sync-categories', [ProductsController::class, 'syncProductCategories'])->name('sync-product-categories'); + Route::delete('/{productId}', [ProductsController::class, 'deleteM2Product'])->name('delete-product'); + Route::delete('/above-m1-max', [ProductsController::class, 'deleteM2ProductsAboveM1Max'])->name('delete-products-above-m1-max'); }); diff --git a/vite.config.js b/vite.config.js index 29fbfe9..c41afea 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,7 +5,15 @@ import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], + input: [ + 'resources/css/app.css', + 'resources/js/app.js', + 'resources/js/connections.js', + 'resources/js/categories.js', + 'resources/js/migration.js', + 'resources/js/attributes.js', + 'resources/js/products.js', + ], refresh: true, }), tailwindcss(),