From 441db95ebc98333c962bf97c57871e874eccd8d1 Mon Sep 17 00:00:00 2001 From: athulrajtflycatchtech Date: Thu, 27 Aug 2026 16:54:12 +0530 Subject: [PATCH] feat(AI Services, Cloud Services & Migrations, Data Analytics & Migration, and Digital Transformation) : add AI, Cloud, Data Analytics, and Digital Transformation management features; include forms and lists for improved content administration --- .../scripts/check-contract-drift.mjs | 4 + apps/Administration-FE/scripts/check-i18n.mjs | 10 + .../scripts/generate-client.mjs | 23 + .../src/components/AdminShell.tsx | 216 ++++++++ .../src/components/AiServiceForm.tsx | 524 ++++++++++++++++++ .../src/components/AiServicesList.tsx | 190 +++++++ .../src/components/CloudServiceForm.tsx | 27 + .../src/components/CloudServicesList.tsx | 23 + .../src/components/DataAnalyticsForm.tsx | 29 + .../src/components/DataAnalyticsList.tsx | 20 + .../components/DigitalTransformationForm.tsx | 386 +++++++++++++ .../components/DigitalTransformationList.tsx | 190 +++++++ .../src/components/NamedPageForm.tsx | 442 +++++++++++++++ .../src/components/NamedPageList.tsx | 233 ++++++++ apps/Administration-FE/src/i18n/en.json | 210 ++++++- apps/Administration-FE/src/lib/admin-api.ts | 124 +++++ .../Administration-FE/src/lib/admin-routes.ts | 16 + .../src/lib/service-page-names.ts | 21 + .../src/pages/admin/ai-services.astro | 4 + .../src/pages/admin/cloud-services.astro | 4 + .../src/pages/admin/data-analytics.astro | 4 + .../pages/admin/digital-transformation.astro | 4 + .../tests/unit/admin-routes.test.ts | 10 + apps/Backend/alembic/env.py | 4 + .../alembic/versions/019_ai_services.py | 60 ++ .../alembic/versions/020_cloud_services.py | 49 ++ .../alembic/versions/021_data_analytics.py | 49 ++ .../versions/022_digital_transformation.py | 50 ++ .../023_ai_services_faq_and_details.py | 67 +++ .../src/flycatch_api/api/admin_ai_services.py | 83 +++ .../flycatch_api/api/admin_cloud_services.py | 87 +++ .../flycatch_api/api/admin_data_analytics.py | 91 +++ .../api/admin_digital_transformation.py | 91 +++ .../flycatch_api/api/public_ai_services.py | 29 + .../flycatch_api/api/public_cloud_services.py | 32 ++ .../flycatch_api/api/public_data_analytics.py | 36 ++ .../api/public_digital_transformation.py | 32 ++ apps/Backend/src/flycatch_api/main.py | 16 + .../src/flycatch_api/models/__init__.py | 9 + .../src/flycatch_api/models/ai_service.py | 75 +++ .../src/flycatch_api/models/cloud_service.py | 39 ++ .../src/flycatch_api/models/data_analytics.py | 39 ++ .../models/digital_transformation.py | 40 ++ .../flycatch_api/schemas/admin_ai_services.py | 93 ++++ .../schemas/admin_digital_transformation.py | 70 +++ .../flycatch_api/schemas/admin_named_pages.py | 108 ++++ .../schemas/admin_solution_details.py | 1 + .../schemas/public_ai_services.py | 47 ++ .../schemas/public_digital_transformation.py | 42 ++ .../schemas/public_named_pages.py | 50 ++ .../services/ai_service_service.py | 303 ++++++++++ .../flycatch_api/services/content_blocks.py | 45 ++ .../digital_transformation_service.py | 251 +++++++++ .../services/named_page_service.py | 248 +++++++++ .../src/flycatch_api/services/page_names.py | 38 ++ .../src/flycatch_api/services/role_service.py | 4 + .../services/solution_detail_service.py | 1 + .../tests/contract/test_openapi_parity.py | 8 + .../integration/test_admin_ai_services.py | 84 +++ .../integration/test_admin_named_pages.py | 80 +++ .../test_admin_solution_details.py | 1 + .../contracts/admin-solution-details.v1.yaml | 3 +- .../contracts/admin-ai-services.v1.yaml | 266 +++++++++ .../contracts/public-ai-services.v1.yaml | 124 +++++ specs/012-admin-ai-services/data-model.md | 36 ++ specs/012-admin-ai-services/quickstart.md | 15 + specs/012-admin-ai-services/spec.md | 53 ++ .../contracts/admin-cloud-services.v1.yaml | 253 +++++++++ .../contracts/public-cloud-services.v1.yaml | 117 ++++ specs/013-admin-cloud-services/data-model.md | 31 ++ specs/013-admin-cloud-services/quickstart.md | 15 + specs/013-admin-cloud-services/spec.md | 51 ++ .../contracts/admin-data-analytics.v1.yaml | 253 +++++++++ .../contracts/public-data-analytics.v1.yaml | 117 ++++ specs/014-admin-data-analytics/data-model.md | 31 ++ specs/014-admin-data-analytics/quickstart.md | 15 + specs/014-admin-data-analytics/spec.md | 51 ++ .../admin-digital-transformation.v1.yaml | 243 ++++++++ .../public-digital-transformation.v1.yaml | 108 ++++ .../data-model.md | 32 ++ .../quickstart.md | 15 + .../015-admin-digital-transformation/spec.md | 51 ++ 82 files changed, 7044 insertions(+), 2 deletions(-) create mode 100644 apps/Administration-FE/src/components/AiServiceForm.tsx create mode 100644 apps/Administration-FE/src/components/AiServicesList.tsx create mode 100644 apps/Administration-FE/src/components/CloudServiceForm.tsx create mode 100644 apps/Administration-FE/src/components/CloudServicesList.tsx create mode 100644 apps/Administration-FE/src/components/DataAnalyticsForm.tsx create mode 100644 apps/Administration-FE/src/components/DataAnalyticsList.tsx create mode 100644 apps/Administration-FE/src/components/DigitalTransformationForm.tsx create mode 100644 apps/Administration-FE/src/components/DigitalTransformationList.tsx create mode 100644 apps/Administration-FE/src/components/NamedPageForm.tsx create mode 100644 apps/Administration-FE/src/components/NamedPageList.tsx create mode 100644 apps/Administration-FE/src/lib/service-page-names.ts create mode 100644 apps/Administration-FE/src/pages/admin/ai-services.astro create mode 100644 apps/Administration-FE/src/pages/admin/cloud-services.astro create mode 100644 apps/Administration-FE/src/pages/admin/data-analytics.astro create mode 100644 apps/Administration-FE/src/pages/admin/digital-transformation.astro create mode 100644 apps/Backend/alembic/versions/019_ai_services.py create mode 100644 apps/Backend/alembic/versions/020_cloud_services.py create mode 100644 apps/Backend/alembic/versions/021_data_analytics.py create mode 100644 apps/Backend/alembic/versions/022_digital_transformation.py create mode 100644 apps/Backend/alembic/versions/023_ai_services_faq_and_details.py create mode 100644 apps/Backend/src/flycatch_api/api/admin_ai_services.py create mode 100644 apps/Backend/src/flycatch_api/api/admin_cloud_services.py create mode 100644 apps/Backend/src/flycatch_api/api/admin_data_analytics.py create mode 100644 apps/Backend/src/flycatch_api/api/admin_digital_transformation.py create mode 100644 apps/Backend/src/flycatch_api/api/public_ai_services.py create mode 100644 apps/Backend/src/flycatch_api/api/public_cloud_services.py create mode 100644 apps/Backend/src/flycatch_api/api/public_data_analytics.py create mode 100644 apps/Backend/src/flycatch_api/api/public_digital_transformation.py create mode 100644 apps/Backend/src/flycatch_api/models/ai_service.py create mode 100644 apps/Backend/src/flycatch_api/models/cloud_service.py create mode 100644 apps/Backend/src/flycatch_api/models/data_analytics.py create mode 100644 apps/Backend/src/flycatch_api/models/digital_transformation.py create mode 100644 apps/Backend/src/flycatch_api/schemas/admin_ai_services.py create mode 100644 apps/Backend/src/flycatch_api/schemas/admin_digital_transformation.py create mode 100644 apps/Backend/src/flycatch_api/schemas/admin_named_pages.py create mode 100644 apps/Backend/src/flycatch_api/schemas/public_ai_services.py create mode 100644 apps/Backend/src/flycatch_api/schemas/public_digital_transformation.py create mode 100644 apps/Backend/src/flycatch_api/schemas/public_named_pages.py create mode 100644 apps/Backend/src/flycatch_api/services/ai_service_service.py create mode 100644 apps/Backend/src/flycatch_api/services/content_blocks.py create mode 100644 apps/Backend/src/flycatch_api/services/digital_transformation_service.py create mode 100644 apps/Backend/src/flycatch_api/services/named_page_service.py create mode 100644 apps/Backend/src/flycatch_api/services/page_names.py create mode 100644 apps/Backend/tests/integration/test_admin_ai_services.py create mode 100644 apps/Backend/tests/integration/test_admin_named_pages.py create mode 100644 specs/012-admin-ai-services/contracts/admin-ai-services.v1.yaml create mode 100644 specs/012-admin-ai-services/contracts/public-ai-services.v1.yaml create mode 100644 specs/012-admin-ai-services/data-model.md create mode 100644 specs/012-admin-ai-services/quickstart.md create mode 100644 specs/012-admin-ai-services/spec.md create mode 100644 specs/013-admin-cloud-services/contracts/admin-cloud-services.v1.yaml create mode 100644 specs/013-admin-cloud-services/contracts/public-cloud-services.v1.yaml create mode 100644 specs/013-admin-cloud-services/data-model.md create mode 100644 specs/013-admin-cloud-services/quickstart.md create mode 100644 specs/013-admin-cloud-services/spec.md create mode 100644 specs/014-admin-data-analytics/contracts/admin-data-analytics.v1.yaml create mode 100644 specs/014-admin-data-analytics/contracts/public-data-analytics.v1.yaml create mode 100644 specs/014-admin-data-analytics/data-model.md create mode 100644 specs/014-admin-data-analytics/quickstart.md create mode 100644 specs/014-admin-data-analytics/spec.md create mode 100644 specs/015-admin-digital-transformation/contracts/admin-digital-transformation.v1.yaml create mode 100644 specs/015-admin-digital-transformation/contracts/public-digital-transformation.v1.yaml create mode 100644 specs/015-admin-digital-transformation/data-model.md create mode 100644 specs/015-admin-digital-transformation/quickstart.md create mode 100644 specs/015-admin-digital-transformation/spec.md diff --git a/apps/Administration-FE/scripts/check-contract-drift.mjs b/apps/Administration-FE/scripts/check-contract-drift.mjs index c9a2a3f..b14fde0 100644 --- a/apps/Administration-FE/scripts/check-contract-drift.mjs +++ b/apps/Administration-FE/scripts/check-contract-drift.mjs @@ -19,6 +19,10 @@ const required = [ 'admin-solutions.v1.ts', 'admin-solution-details.v1.ts', 'admin-solution-products.v1.ts', + 'admin-ai-services.v1.ts', + 'admin-cloud-services.v1.ts', + 'admin-data-analytics.v1.ts', + 'admin-digital-transformation.v1.ts', ]; let failed = false; diff --git a/apps/Administration-FE/scripts/check-i18n.mjs b/apps/Administration-FE/scripts/check-i18n.mjs index 9ba5a47..deeda43 100644 --- a/apps/Administration-FE/scripts/check-i18n.mjs +++ b/apps/Administration-FE/scripts/check-i18n.mjs @@ -32,6 +32,16 @@ const files = [ 'components/SolutionProductsList.tsx', 'components/SolutionProductForm.tsx', 'components/SeoFields.tsx', + 'components/NamedPageList.tsx', + 'components/NamedPageForm.tsx', + 'components/AiServicesList.tsx', + 'components/AiServiceForm.tsx', + 'components/CloudServicesList.tsx', + 'components/CloudServiceForm.tsx', + 'components/DataAnalyticsList.tsx', + 'components/DataAnalyticsForm.tsx', + 'components/DigitalTransformationList.tsx', + 'components/DigitalTransformationForm.tsx', 'components/CaseStudiesList.tsx', 'components/CaseStudyForm.tsx', 'components/IndustriesList.tsx', diff --git a/apps/Administration-FE/scripts/generate-client.mjs b/apps/Administration-FE/scripts/generate-client.mjs index fae2919..89dd09a 100644 --- a/apps/Administration-FE/scripts/generate-client.mjs +++ b/apps/Administration-FE/scripts/generate-client.mjs @@ -23,6 +23,13 @@ const solutionProductsContractsDir = join( repoRoot, 'specs/011-admin-solution-products/contracts', ); +const aiServicesContractsDir = join(repoRoot, 'specs/012-admin-ai-services/contracts'); +const cloudServicesContractsDir = join(repoRoot, 'specs/013-admin-cloud-services/contracts'); +const dataAnalyticsContractsDir = join(repoRoot, 'specs/014-admin-data-analytics/contracts'); +const digitalTransformationContractsDir = join( + repoRoot, + 'specs/015-admin-digital-transformation/contracts', +); const contractSets = [ { @@ -67,6 +74,22 @@ const contractSets = [ dir: solutionProductsContractsDir, files: ['admin-solution-products.v1.yaml'], }, + { + dir: aiServicesContractsDir, + files: ['admin-ai-services.v1.yaml'], + }, + { + dir: cloudServicesContractsDir, + files: ['admin-cloud-services.v1.yaml'], + }, + { + dir: dataAnalyticsContractsDir, + files: ['admin-data-analytics.v1.yaml'], + }, + { + dir: digitalTransformationContractsDir, + files: ['admin-digital-transformation.v1.yaml'], + }, ]; mkdirSync(outDir, { recursive: true }); diff --git a/apps/Administration-FE/src/components/AdminShell.tsx b/apps/Administration-FE/src/components/AdminShell.tsx index 3abcfbc..8675018 100644 --- a/apps/Administration-FE/src/components/AdminShell.tsx +++ b/apps/Administration-FE/src/components/AdminShell.tsx @@ -47,6 +47,14 @@ import SolutionDetailsList from './SolutionDetailsList'; import SolutionDetailForm from './SolutionDetailForm'; import SolutionProductsList from './SolutionProductsList'; import SolutionProductForm from './SolutionProductForm'; +import AiServicesList from './AiServicesList'; +import AiServiceForm from './AiServiceForm'; +import CloudServicesList from './CloudServicesList'; +import CloudServiceForm from './CloudServiceForm'; +import DataAnalyticsList from './DataAnalyticsList'; +import DataAnalyticsForm from './DataAnalyticsForm'; +import DigitalTransformationList from './DigitalTransformationList'; +import DigitalTransformationForm from './DigitalTransformationForm'; type View = AdminView; @@ -80,6 +88,10 @@ function applyRoute( setEditingSolutionId: (id: string | null) => void; setEditingSolutionDetailId: (id: string | null) => void; setEditingSolutionProductId: (id: string | null) => void; + setEditingAiServiceId: (id: string | null) => void; + setEditingCloudServiceId: (id: string | null) => void; + setEditingDataAnalyticId: (id: string | null) => void; + setEditingDigitalTransformationId: (id: string | null) => void; }, ) { setters.setView(route.view); @@ -103,6 +115,12 @@ function applyRoute( setters.setEditingSolutionProductId( route.view === 'solution_product_form' ? route.editingId : null, ); + setters.setEditingAiServiceId(route.view === 'ai_service_form' ? route.editingId : null); + setters.setEditingCloudServiceId(route.view === 'cloud_service_form' ? route.editingId : null); + setters.setEditingDataAnalyticId(route.view === 'data_analytics_form' ? route.editingId : null); + setters.setEditingDigitalTransformationId( + route.view === 'digital_transformation_form' ? route.editingId : null, + ); } export default function AdminShell() { @@ -169,6 +187,24 @@ export default function AdminShell() { const route = readAdminLocation(); return route.view === 'solution_product_form' ? route.editingId : null; }); + const [editingAiServiceId, setEditingAiServiceId] = useState(() => { + const route = readAdminLocation(); + return route.view === 'ai_service_form' ? route.editingId : null; + }); + const [editingCloudServiceId, setEditingCloudServiceId] = useState(() => { + const route = readAdminLocation(); + return route.view === 'cloud_service_form' ? route.editingId : null; + }); + const [editingDataAnalyticId, setEditingDataAnalyticId] = useState(() => { + const route = readAdminLocation(); + return route.view === 'data_analytics_form' ? route.editingId : null; + }); + const [editingDigitalTransformationId, setEditingDigitalTransformationId] = useState< + string | null + >(() => { + const route = readAdminLocation(); + return route.view === 'digital_transformation_form' ? route.editingId : null; + }); const [session, setSession] = useState(null); const [siteSettings, setSiteSettings] = useState | null>(null); const [message, setMessage] = useState(null); @@ -221,6 +257,10 @@ export default function AdminShell() { setEditingSolutionId, setEditingSolutionDetailId, setEditingSolutionProductId, + setEditingAiServiceId, + setEditingCloudServiceId, + setEditingDataAnalyticId, + setEditingDigitalTransformationId, }); }, []); @@ -302,6 +342,22 @@ export default function AdminShell() { document.title = t('admin.workspace.solution_products'); return; } + if (view === 'ai_services' || view === 'ai_service_form') { + document.title = t('admin.workspace.ai_services'); + return; + } + if (view === 'cloud_services' || view === 'cloud_service_form') { + document.title = t('admin.workspace.cloud_services'); + return; + } + if (view === 'data_analytics' || view === 'data_analytics_form') { + document.title = t('admin.workspace.data_analytics'); + return; + } + if (view === 'digital_transformation' || view === 'digital_transformation_form') { + document.title = t('admin.workspace.digital_transformation'); + return; + } if (view === 'blogs' || view === 'blog_form') { document.title = t('admin.workspace.blogs'); return; @@ -465,6 +521,22 @@ export default function AdminShell() { openList('solution_products'); } + function openAiServices() { + openList('ai_services'); + } + + function openCloudServices() { + openList('cloud_services'); + } + + function openDataAnalytics() { + openList('data_analytics'); + } + + function openDigitalTransformation() { + openList('digital_transformation'); + } + function onNavClick(event: MouseEvent, href: string) { if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) { return; @@ -498,6 +570,10 @@ export default function AdminShell() { const canPublishSolutions = hasPermission(session, 'solutions.publish'); const canPublishSolutionDetails = hasPermission(session, 'solution_details.publish'); const canPublishSolutionProducts = hasPermission(session, 'solution_products.publish'); + const canPublishAiServices = hasPermission(session, 'ai_services.publish'); + const canPublishCloudServices = hasPermission(session, 'cloud_services.publish'); + const canPublishDataAnalytics = hasPermission(session, 'data_analytics.publish'); + const canPublishDigitalTransformation = hasPermission(session, 'digital_transformation.publish'); const canManageRoles = canManageRolesFrom(session); const settingsCurrent = view === 'roles' || view === 'role_form'; const initial = session.email.slice(0, 1).toUpperCase(); @@ -506,6 +582,10 @@ export default function AdminShell() { const canReadSolutions = canReadResource(session, 'solutions'); const canReadSolutionDetails = canReadResource(session, 'solution_details'); const canReadSolutionProducts = canReadResource(session, 'solution_products'); + const canReadAiServices = canReadResource(session, 'ai_services'); + const canReadCloudServices = canReadResource(session, 'cloud_services'); + const canReadDataAnalytics = canReadResource(session, 'data_analytics'); + const canReadDigitalTransformation = canReadResource(session, 'digital_transformation'); const canReadBlogs = canReadResource(session, 'blogs'); const canReadCaseStudies = canReadResource(session, 'case_studies'); const canReadIndustries = canReadResource(session, 'industries'); @@ -523,6 +603,10 @@ export default function AdminShell() { canReadSolutions || canReadSolutionDetails || canReadSolutionProducts || + canReadAiServices || + canReadCloudServices || + canReadDataAnalytics || + canReadDigitalTransformation || canReadBlogs || canReadCaseStudies || canReadIndustries || @@ -637,6 +721,72 @@ export default function AdminShell() { )} + {canReadAiServices && ( +
  • + onNavClick(event, adminListHref('ai_services'))} + > + {t('admin.workspace.ai_services')} + +
  • + )} + {canReadCloudServices && ( +
  • + onNavClick(event, adminListHref('cloud_services'))} + > + {t('admin.workspace.cloud_services')} + +
  • + )} + {canReadDataAnalytics && ( +
  • + onNavClick(event, adminListHref('data_analytics'))} + > + {t('admin.workspace.data_analytics')} + +
  • + )} + {canReadDigitalTransformation && ( +
  • + onNavClick(event, adminListHref('digital_transformation'))} + > + {t('admin.workspace.digital_transformation')} + +
  • + )} {canReadBlogs && (
  • )} + {view === 'ai_services' && ( + openForm('ai_services', null)} + onEdit={(id) => openForm('ai_services', id)} + /> + )} + {view === 'ai_service_form' && ( + afterListSave(openAiServices, t('admin.ai_services.saved'))} + /> + )} + {view === 'cloud_services' && ( + openForm('cloud_services', null)} + onEdit={(id) => openForm('cloud_services', id)} + /> + )} + {view === 'cloud_service_form' && ( + afterListSave(openCloudServices, t('admin.cloud_services.saved'))} + /> + )} + {view === 'data_analytics' && ( + openForm('data_analytics', null)} + onEdit={(id) => openForm('data_analytics', id)} + /> + )} + {view === 'data_analytics_form' && ( + afterListSave(openDataAnalytics, t('admin.data_analytics.saved'))} + /> + )} + {view === 'digital_transformation' && ( + openForm('digital_transformation', null)} + onEdit={(id) => openForm('digital_transformation', id)} + /> + )} + {view === 'digital_transformation_form' && ( + + afterListSave(openDigitalTransformation, t('admin.digital_transformation.saved')) + } + /> + )} {view === 'blogs' && ( void; + onSaved: () => void; +} + +type IndustryDraft = { + key: string; + title: string; + image_key: string | null; + imageFile: File | null; + order: number; +}; + +type AccordionDraft = { key: string; title: string; contents: string; order: number }; + +function nextKey(): string { + return `${Date.now()}-${Math.random().toString(16).slice(2)}`; +} + +function emptyIndustry(): IndustryDraft { + return { key: nextKey(), title: '', image_key: null, imageFile: null, order: 0 }; +} + +function emptyAccordion(): AccordionDraft { + return { key: nextKey(), title: '', contents: '', order: 0 }; +} + +export default function AiServiceForm({ entryId, canPublish, onCancel, onSaved }: Props) { + const [bannerTitle, setBannerTitle] = useState(''); + const [bannerKey, setBannerKey] = useState(null); + const [bannerFile, setBannerFile] = useState(null); + const [introductionTitle, setIntroductionTitle] = useState(''); + const [introductionDescription, setIntroductionDescription] = useState(''); + const [solutionsTitle, setSolutionsTitle] = useState(''); + const [solutionsDescription, setSolutionsDescription] = useState(''); + const [industryTitle, setIndustryTitle] = useState(''); + const [industryDescription, setIndustryDescription] = useState(''); + const [industryItems, setIndustryItems] = useState([emptyIndustry()]); + const [expertiseTitle, setExpertiseTitle] = useState(''); + const [expertiseKey, setExpertiseKey] = useState(null); + const [expertiseFile, setExpertiseFile] = useState(null); + const [expertiseAccordion, setExpertiseAccordion] = useState([emptyAccordion()]); + const [expertiseAccordionDescription, setExpertiseAccordionDescription] = useState(''); + const [solutionIds, setSolutionIds] = useState([]); + const [solutionOptions, setSolutionOptions] = useState<{ id: string; name: string }[]>([]); + const [faqTitle, setFaqTitle] = useState(''); + const [faqDescription, setFaqDescription] = useState(''); + const [faqAccordion, setFaqAccordion] = useState([emptyAccordion()]); + const [seo, setSeo] = useState(emptySeo); + const [seoImageFile, setSeoImageFile] = useState(null); + const [status, setStatus] = useState<'draft' | 'publish'>('draft'); + const [error, setError] = useState(null); + const [ready, setReady] = useState(!entryId); + const [saving, setSaving] = useState(false); + + useEffect(() => { + async function loadSolutionDetails() { + const options: { id: string; name: string }[] = []; + let page = 1; + while (true) { + const result = await listSolutionDetails('', page); + options.push( + ...result.items.map((item) => ({ id: item.id, name: item.title || item.id })), + ); + if (page * result.per_page >= result.total) break; + page += 1; + } + setSolutionOptions(options); + } + loadSolutionDetails().catch(() => undefined); + }, []); + + useEffect(() => { + if (!entryId) return; + getAiService(entryId) + .then(async (item) => { + setBannerTitle(item.banner_title); + setBannerKey(item.banner_image_key); + setIntroductionTitle(item.introduction_title); + setIntroductionDescription(item.introduction_description); + setSolutionsTitle(item.solutions_title); + setSolutionsDescription(item.solutions_description); + setIndustryTitle(item.industry_title); + setIndustryDescription(item.industry_description); + setIndustryItems( + item.industry_items.length + ? item.industry_items.map((row) => ({ + key: nextKey(), + title: row.title, + image_key: row.image_key ?? null, + imageFile: null, + order: row.order, + })) + : [emptyIndustry()], + ); + setExpertiseTitle(item.ai_expertise_title); + setExpertiseKey(item.ai_expertise_image_key); + setExpertiseAccordion( + item.ai_expertise_accordion.length + ? await Promise.all( + item.ai_expertise_accordion.map(async (row) => ({ + key: nextKey(), + title: row.title, + contents: await hydrateRichText(row.contents), + order: row.order, + })), + ) + : [emptyAccordion()], + ); + setExpertiseAccordionDescription( + await hydrateRichText(item.ai_expertise_accordion_description), + ); + setSolutionIds(item.solution_ids); + setFaqTitle(item.faq_title); + setFaqDescription(await hydrateRichText(item.faq_description)); + setFaqAccordion( + item.faq_accordion.length + ? await Promise.all( + item.faq_accordion.map(async (row) => ({ + key: nextKey(), + title: row.title, + contents: await hydrateRichText(row.contents), + order: row.order, + })), + ) + : [emptyAccordion()], + ); + setSeo(seoValue(item.seo)); + setStatus(item.status); + setReady(true); + }) + .catch(() => { + setError(t('admin.workspace.request_failed')); + setReady(true); + }); + }, [entryId]); + + async function persist(nextStatus: 'draft' | 'publish') { + setError(null); + setSaving(true); + try { + const nextBanner = bannerFile ? (await uploadMedia(bannerFile)).key : bannerKey; + const nextExpertise = expertiseFile ? (await uploadMedia(expertiseFile)).key : expertiseKey; + const nextSeoImage = seoImageFile ? (await uploadMedia(seoImageFile)).key : seo.image_key; + const industries = []; + for (const [index, item] of industryItems.entries()) { + const image_key = item.imageFile ? (await uploadMedia(item.imageFile)).key : item.image_key; + industries.push({ title: item.title.trim(), image_key, order: index }); + } + const payload: AiServiceWrite = { + banner_title: bannerTitle.trim(), + banner_image_key: nextBanner, + introduction_title: introductionTitle.trim(), + introduction_description: introductionDescription.trim(), + solutions_title: solutionsTitle.trim(), + solutions_description: solutionsDescription.trim(), + industry_title: industryTitle.trim(), + industry_description: industryDescription.trim(), + industry_items: industries, + ai_expertise_title: expertiseTitle.trim(), + ai_expertise_image_key: nextExpertise, + ai_expertise_accordion: expertiseAccordion.map((item, index) => ({ + title: item.title.trim(), + contents: persistRichText(item.contents), + order: index, + })), + ai_expertise_accordion_description: persistRichText(expertiseAccordionDescription), + solution_ids: solutionIds, + faq_title: faqTitle.trim(), + faq_description: persistRichText(faqDescription), + faq_accordion: faqAccordion.map((item, index) => ({ + title: item.title.trim(), + contents: persistRichText(item.contents), + order: index, + })), + seo: { ...seo, image_key: nextSeoImage }, + status: nextStatus, + }; + if (entryId) await updateAiService(entryId, payload); + else await createAiService(payload); + onSaved(); + } catch (caught) { + setError(apiErrorMessage(caught)); + } finally { + setSaving(false); + } + } + + if (!ready) { + return ( +
    +

    +

    +
    + ); + } + + return ( +
    +
    +

    {entryId ? t('admin.ai_services.edit') : t('admin.ai_services.add')}

    +
    +
    { + event.preventDefault(); + persist(status).catch(() => undefined); + }} + > + + + + +