diff --git a/application/src/components/settings/about-system/AboutSystem.tsx b/application/src/components/settings/about-system/AboutSystem.tsx index 677d2dd..e3fc5ec 100644 --- a/application/src/components/settings/about-system/AboutSystem.tsx +++ b/application/src/components/settings/about-system/AboutSystem.tsx @@ -1,50 +1,293 @@ -import React, { useEffect, useState } from 'react'; -import { format } from 'date-fns'; -import { - Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; +import React, { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; -import { - Github, FileText, Twitter, MessageCircle, Code2, ServerIcon, -} from "lucide-react"; +import { Github, FileText, Twitter, MessageCircle, Code2, ServerIcon, FolderOpen, Database, CheckCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; import { useLanguage } from "@/contexts/LanguageContext"; import { useTheme } from "@/contexts/ThemeContext"; import { useSystemSettings } from "@/hooks/useSystemSettings"; +import { pb } from "@/lib/pocketbase"; +import { toast } from "@/components/ui/use-toast"; export const AboutSystem: React.FC = () => { const { t } = useLanguage(); const { theme } = useTheme(); const { systemName } = useSystemSettings(); + const [isImporting, setIsImporting] = useState(false); + const [mergeFields, setMergeFields] = useState(true); + const [importResult, setImportResult] = useState<{ + success: boolean; + created: number; + updated: number; + skipped: number; + } | null>(null); - const [version, setVersion] = useState('...'); - const [releaseDate, setReleaseDate] = useState('...'); + // Helper function to check if a collection is a system collection + const isSystemCollection = (name: string): boolean => { + return name.startsWith('_') || ['users'].includes(name); + }; - useEffect(() => { - const fetchLatestRelease = async () => { - try { - const res = await fetch('https://api.github.com/repos/operacle/checkcle/releases/latest'); - const data = await res.json(); - setVersion(data.tag_name || 'v1.x.x'); - setReleaseDate(data.published_at ? format(new Date(data.published_at), 'MMMM d, yyyy') : t('unknown')); - } catch (err) { - setVersion('v1.x.x'); - setReleaseDate(t('unknown')); + // Helper function to validate collection schema + const validateCollectionSchema = (collection: any): string | null => { + if (!collection.name) return "Collection name is required"; + if (typeof collection.name !== 'string') return "Collection name must be a string"; + if (!/^[a-z][a-z0-9_]*$/.test(collection.name)) { + return "Collection name must start with a letter and contain only lowercase letters, numbers, and underscores"; + } + if (collection.name.length < 3 || collection.name.length > 32) { + return "Collection name must be between 3 and 32 characters"; + } + + // Validate schema array if present + if (collection.schema && !Array.isArray(collection.schema)) { + return "Collection schema must be an array"; + } + + // Validate each field in the schema + if (collection.schema) { + for (const field of collection.schema) { + if (!field.name || typeof field.name !== 'string') { + return `Invalid field name in collection '${collection.name}'`; + } + if (!field.type || typeof field.type !== 'string') { + return `Invalid field type for '${field.name}' in collection '${collection.name}'`; + } } - }; - fetchLatestRelease(); - }, [t]); + } + + return null; + }; + + const handleSchemaImport = async (schemaData: string) => { + if (!schemaData.trim()) { + toast({ + title: "Error", + description: "No schema data provided", + variant: "destructive", + }); + return; + } + + try { + setIsImporting(true); + setImportResult(null); + + // Parse the JSON to validate it + const collections = JSON.parse(schemaData); + + if (!Array.isArray(collections)) { + throw new Error("Schema must be an array of collections"); + } + + // Check if user is authenticated as admin + if (!pb.authStore.isValid) { + throw new Error("Authentication required. Please log in as an admin."); + } + // Create/update collections one by one using PocketBase client + let successCount = 0; + let errorCount = 0; + let updatedCount = 0; + let skippedCount = 0; + const errors = []; + + for (const collection of collections) { + try { + // Skip system collections + if (isSystemCollection(collection.name)) { + skippedCount++; + continue; + } + + // Validate collection schema + const validationError = validateCollectionSchema(collection); + if (validationError) { + throw new Error(validationError); + } + // Check if collection already exists + const existingCollections = await pb.collections.getFullList(); + const existingCollection = existingCollections.find(c => c.name === collection.name); + + if (existingCollection) { + if (mergeFields) { + + try { + // Get existing schema + const existingSchema = existingCollection.schema || []; + const newSchema = collection.schema || []; + // Merge schemas - add new fields, keep existing ones + const mergedSchema = [...existingSchema]; + let fieldsAdded = 0; + + for (const newField of newSchema) { + const existingFieldIndex = mergedSchema.findIndex(f => f.name === newField.name); + if (existingFieldIndex >= 0) { + } else { + // Add new field + mergedSchema.push(newField); + fieldsAdded++; + } + } + + // Update the collection with merged schema + const updateData = { + ...collection, + schema: mergedSchema + }; + + const updatedCollection = await pb.collections.update(existingCollection.id, updateData); + updatedCount++; + } catch (mergeError) { + throw new Error(`Failed to merge fields: ${mergeError.message || mergeError}`); + } + } else { + skippedCount++; + continue; + } + } else { + // Create new collection + try { + const newCollection = await pb.collections.create(collection); + successCount++; + } catch (createError) { + + // Extract more detailed error information + let errorMessage = createError.message || createError; + if (createError.data) { + // Try to extract field-specific errors + if (createError.data.data) { + const fieldErrors = Object.entries(createError.data.data).map(([field, msgs]) => + `${field}: ${Array.isArray(msgs) ? msgs.join(', ') : msgs}` + ).join('; '); + if (fieldErrors) { + errorMessage += ` - Field errors: ${fieldErrors}`; + } + } + } + + throw new Error(`Failed to create collection: ${errorMessage}`); + } + } + } catch (error) { + errorCount++; + const errorMsg = `Failed to process collection '${collection.name}': ${error.message || error}`; + errors.push(errorMsg); + } + } + // Set import result for display + setImportResult({ + success: successCount + updatedCount > 0, + created: successCount, + updated: updatedCount, + skipped: skippedCount + }); + + // Show results + const totalProcessed = successCount + updatedCount; + if (totalProcessed > 0 || skippedCount > 0) { + let description = ''; + if (totalProcessed > 0) { + description = `Successfully processed ${totalProcessed} collection(s)`; + if (successCount > 0) description += ` (${successCount} created)`; + if (updatedCount > 0) description += ` (${updatedCount} updated)`; + } + if (skippedCount > 0) { + if (description) description += `, `; + description += `${skippedCount} skipped (system collections or existing)`; + } + if (errorCount > 0) description += `. ${errorCount} failed.`; + + toast({ + title: "Import Complete", + description, + variant: totalProcessed > errorCount ? "default" : "destructive", + }); + } else if (errorCount > 0) { + toast({ + title: "Import Failed", + description: "No collections were processed successfully.", + variant: "destructive", + }); + } else { + toast({ + title: "No Changes", + description: "All collections already exist and no changes were made.", + variant: "default", + }); + } + + if (errors.length > 0) { + // Show detailed errors for debugging + if (errors.length <= 3) { + toast({ + title: "Detailed Errors", + description: errors.join('; '), + variant: "destructive", + }); + } + } + + } catch (error) { + toast({ + title: "Import Failed", + description: error instanceof Error ? error.message : "Failed to import schema", + variant: "destructive", + }); + setImportResult({ + success: false, + created: 0, + updated: 0, + skipped: 0 + }); + } finally { + setIsImporting(false); + } + }; + + const handleLoadLocalSchema = async () => { + try { + setIsImporting(true); + setImportResult(null); + + const response = await fetch('/upload/data/pb_schema_latest.json'); + + if (!response.ok) { + throw new Error(`Failed to load local schema: ${response.status} ${response.statusText}`); + } + + const schemaText = await response.text(); + + // Validate that it's valid JSON + JSON.parse(schemaText); + + // Directly import the schema + await handleSchemaImport(schemaText); + + } catch (error) { + toast({ + title: "Import Failed", + description: error instanceof Error ? error.message : "Failed to load and import local schema file", + variant: "destructive", + }); + setImportResult({ + success: false, + created: 0, + updated: 0, + skipped: 0 + }); + setIsImporting(false); + } + }; return (

{t('aboutSystem')}

-

-

- + - +
@@ -58,7 +301,7 @@ export const AboutSystem: React.FC = () => {
{t('systemVersion')} - {version} + {t('version')} 1.5.1
@@ -68,22 +311,20 @@ export const AboutSystem: React.FC = () => {
{t('releasedOn')} - {releaseDate} + Auguest 21, 2025
- + {t('links')} - - {systemName || 'CheckCle'} {t('resources').toLowerCase()} - + {systemName || 'CheckCle'} {t('resources').toLowerCase()}
@@ -107,8 +348,90 @@ export const AboutSystem: React.FC = () => {
+ + + + + + + + Update Schema + + Automatic import collections schema + + +
+
+ setMergeFields(checked === true)} + /> + +
+
+ +
+ +
+ + {importResult && ( +
+
+ + + {importResult.success ? 'Import Successful' : 'Import Failed'} + +
+
+ {importResult.created > 0 && `${importResult.created} collections created`} + {importResult.updated > 0 && (importResult.created > 0 ? ', ' : '') + `${importResult.updated} collections updated`} + {importResult.skipped > 0 && ((importResult.created > 0 || importResult.updated > 0) ? ', ' : '') + `${importResult.skipped} collections skipped`} +
+
+ )} + +
+

+ Instructions: +

+
    +
  • Merge fields: Safely add new fields to existing collections, preserves all data
  • +
  • System collections (starting with _) and users collection will be skipped automatically
  • +
  • Only authenticated admins can perform schema imports
  • +
+
+
+
); }; -export default AboutSystem; +export default AboutSystem; \ No newline at end of file