Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

1262 строки
40KB

  1. package handlers
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "regexp"
  8. "sort"
  9. "strconv"
  10. "strings"
  11. "unicode"
  12. "github.com/go-chi/chi/v5"
  13. "qctextbuilder/internal/buildsvc"
  14. "qctextbuilder/internal/config"
  15. "qctextbuilder/internal/domain"
  16. "qctextbuilder/internal/draftsvc"
  17. "qctextbuilder/internal/onboarding"
  18. "qctextbuilder/internal/templatesvc"
  19. )
  20. type UI struct {
  21. templateSvc *templatesvc.Service
  22. onboardSvc *onboarding.Service
  23. draftSvc *draftsvc.Service
  24. buildSvc buildsvc.Service
  25. cfg config.Config
  26. render htmlRenderer
  27. }
  28. type htmlRenderer interface {
  29. Render(w http.ResponseWriter, name string, data any)
  30. }
  31. type pageData struct {
  32. Title string
  33. Msg string
  34. Err string
  35. Current string
  36. }
  37. type homePageData struct {
  38. pageData
  39. TemplateCount int
  40. }
  41. type settingsPageData struct {
  42. pageData
  43. QCBaseURL string
  44. PollIntervalSeconds int
  45. PollTimeoutSeconds int
  46. PollMaxConcurrent int
  47. TokenConfigured bool
  48. LanguageOutputMode string
  49. }
  50. type templatesPageData struct {
  51. pageData
  52. Templates []domain.Template
  53. }
  54. type templateFieldView struct {
  55. Path string
  56. FieldKind string
  57. IsEnabled bool
  58. IsRequiredByUs bool
  59. DisplayLabel string
  60. DisplayOrder int
  61. WebsiteSection string
  62. Notes string
  63. SampleValue string
  64. }
  65. type websiteSectionOptionView struct {
  66. Value string
  67. Label string
  68. }
  69. type templateDetailPageData struct {
  70. pageData
  71. Detail *templatesvc.TemplateDetail
  72. Fields []templateFieldView
  73. WebsiteSectionOptions []websiteSectionOptionView
  74. }
  75. type buildFieldView struct {
  76. Index int
  77. Path string
  78. DisplayLabel string
  79. SampleValue string
  80. Value string
  81. }
  82. type buildFieldGroupView struct {
  83. Title string
  84. Fields []buildFieldView
  85. }
  86. type buildFieldSectionView struct {
  87. Key string
  88. Title string
  89. Description string
  90. EditableGroups []buildFieldGroupView
  91. EditableFields []buildFieldView
  92. DisabledFields []buildFieldView
  93. }
  94. type pendingField struct {
  95. Field domain.TemplateField
  96. View buildFieldView
  97. }
  98. type fieldRole struct {
  99. Label string
  100. Order int
  101. }
  102. var blockIDPattern = regexp.MustCompile(`(?i)(?:^|[_.])([mcr]\d{3,})(?:[_.]|$)`)
  103. var looseBlockIDPattern = regexp.MustCompile(`(?i)([mcr]\d{3,})`)
  104. var knownBlockAreas = map[string]string{
  105. "m1710": "Hero / Haupttitel",
  106. "c7886": "Intro / Einleitung",
  107. "r4830": "Services",
  108. "m4178": "Gallery / Medien",
  109. "c2929": "Ueber uns / About",
  110. "r4748": "Team",
  111. "r1508": "Testimonials",
  112. "c1165": "CTA / Highlight / Banner",
  113. }
  114. type buildNewPageData struct {
  115. pageData
  116. Templates []domain.Template
  117. Drafts []domain.BuildDraft
  118. SelectedDraftID string
  119. SelectedTemplateID int64
  120. SelectedManifestID string
  121. FieldSections []buildFieldSectionView
  122. EditableFields []buildFieldView
  123. EnabledFields []buildFieldView
  124. Form buildFormInput
  125. }
  126. type buildFormInput struct {
  127. DraftID string
  128. DraftSource string
  129. DraftStatus string
  130. DraftNotes string
  131. RequestName string
  132. CompanyName string
  133. BusinessType string
  134. Username string
  135. Email string
  136. Phone string
  137. OrgNumber string
  138. StartDate string
  139. Mission string
  140. DescriptionShort string
  141. DescriptionLong string
  142. SiteLanguage string
  143. AddressLine1 string
  144. AddressLine2 string
  145. AddressCity string
  146. AddressRegion string
  147. AddressZIP string
  148. AddressCountry string
  149. }
  150. type buildDetailPageData struct {
  151. pageData
  152. Build *domain.SiteBuild
  153. EffectiveGlobal []byte
  154. CanPoll bool
  155. CanFetchEditorURL bool
  156. AutoRefreshSeconds int
  157. }
  158. func NewUI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service, cfg config.Config, render htmlRenderer) *UI {
  159. return &UI{templateSvc: templateSvc, onboardSvc: onboardSvc, draftSvc: draftSvc, buildSvc: buildSvc, cfg: cfg, render: render}
  160. }
  161. func (u *UI) Home(w http.ResponseWriter, r *http.Request) {
  162. templates, err := u.templateSvc.ListTemplates(r.Context())
  163. if err != nil {
  164. u.render.Render(w, "home", homePageData{pageData: basePageData(r, "Home", "/"), TemplateCount: 0})
  165. return
  166. }
  167. u.render.Render(w, "home", homePageData{pageData: basePageData(r, "Home", "/"), TemplateCount: len(templates)})
  168. }
  169. func (u *UI) Settings(w http.ResponseWriter, r *http.Request) {
  170. u.render.Render(w, "settings", settingsPageData{
  171. pageData: basePageData(r, "Settings", "/settings"),
  172. QCBaseURL: u.cfg.QCBaseURL,
  173. PollIntervalSeconds: u.cfg.PollIntervalSeconds,
  174. PollTimeoutSeconds: u.cfg.PollTimeoutSeconds,
  175. PollMaxConcurrent: u.cfg.PollMaxConcurrent,
  176. TokenConfigured: strings.TrimSpace(u.cfg.QCToken) != "",
  177. LanguageOutputMode: "EN",
  178. })
  179. }
  180. func (u *UI) Templates(w http.ResponseWriter, r *http.Request) {
  181. templates, err := u.templateSvc.ListTemplates(r.Context())
  182. if err != nil {
  183. http.Error(w, err.Error(), http.StatusBadRequest)
  184. return
  185. }
  186. u.render.Render(w, "templates", templatesPageData{pageData: basePageData(r, "Templates", "/templates"), Templates: templates})
  187. }
  188. func (u *UI) SyncTemplates(w http.ResponseWriter, r *http.Request) {
  189. if _, err := u.templateSvc.SyncAITemplates(r.Context()); err != nil {
  190. http.Redirect(w, r, "/templates?err="+urlQuery(err.Error()), http.StatusSeeOther)
  191. return
  192. }
  193. http.Redirect(w, r, "/templates?msg=sync+done", http.StatusSeeOther)
  194. }
  195. func (u *UI) TemplateDetail(w http.ResponseWriter, r *http.Request) {
  196. templateID, ok := parseTemplateID(w, r)
  197. if !ok {
  198. return
  199. }
  200. detail, err := u.templateSvc.GetTemplateDetail(r.Context(), templateID)
  201. if err != nil {
  202. http.Error(w, err.Error(), http.StatusNotFound)
  203. return
  204. }
  205. fields := make([]templateFieldView, 0, len(detail.Fields))
  206. for _, f := range detail.Fields {
  207. fields = append(fields, templateFieldView{
  208. Path: f.Path,
  209. FieldKind: f.FieldKind,
  210. IsEnabled: f.IsEnabled,
  211. IsRequiredByUs: f.IsRequiredByUs,
  212. DisplayLabel: f.DisplayLabel,
  213. DisplayOrder: f.DisplayOrder,
  214. WebsiteSection: domain.NormalizeWebsiteSection(f.WebsiteSection),
  215. Notes: f.Notes,
  216. SampleValue: f.SampleValue,
  217. })
  218. }
  219. u.render.Render(w, "template_detail", templateDetailPageData{
  220. pageData: basePageData(r, "Template Detail", "/templates"),
  221. Detail: detail,
  222. Fields: fields,
  223. WebsiteSectionOptions: websiteSectionOptions(),
  224. })
  225. }
  226. func (u *UI) OnboardTemplate(w http.ResponseWriter, r *http.Request) {
  227. templateID, ok := parseTemplateID(w, r)
  228. if !ok {
  229. return
  230. }
  231. if _, _, err := u.onboardSvc.OnboardTemplate(r.Context(), templateID); err != nil {
  232. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery(err.Error())), http.StatusSeeOther)
  233. return
  234. }
  235. http.Redirect(w, r, fmt.Sprintf("/templates/%d?msg=onboarding+done", templateID), http.StatusSeeOther)
  236. }
  237. func (u *UI) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) {
  238. templateID, ok := parseTemplateID(w, r)
  239. if !ok {
  240. return
  241. }
  242. if err := r.ParseForm(); err != nil {
  243. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid form")), http.StatusSeeOther)
  244. return
  245. }
  246. count, _ := strconv.Atoi(r.FormValue("field_count"))
  247. patches := make([]onboarding.FieldPatch, 0, count)
  248. for i := 0; i < count; i++ {
  249. path := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_path_%d", i)))
  250. if path == "" {
  251. continue
  252. }
  253. enabled := r.FormValue(fmt.Sprintf("field_enabled_%d", i)) == "on"
  254. required := r.FormValue(fmt.Sprintf("field_required_%d", i)) == "on"
  255. label := r.FormValue(fmt.Sprintf("field_label_%d", i))
  256. notes := r.FormValue(fmt.Sprintf("field_notes_%d", i))
  257. websiteSection := r.FormValue(fmt.Sprintf("field_website_section_%d", i))
  258. order, err := strconv.Atoi(strings.TrimSpace(r.FormValue(fmt.Sprintf("field_order_%d", i))))
  259. if err != nil {
  260. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid display order")), http.StatusSeeOther)
  261. return
  262. }
  263. patches = append(patches, onboarding.FieldPatch{
  264. Path: path,
  265. IsEnabled: boolPtr(enabled),
  266. IsRequiredByUs: boolPtr(required),
  267. DisplayLabel: strPtr(label),
  268. DisplayOrder: intPtr(order),
  269. WebsiteSection: strPtr(websiteSection),
  270. Notes: strPtr(notes),
  271. })
  272. }
  273. manifestID := r.FormValue("manifest_id")
  274. if _, _, err := u.onboardSvc.UpdateTemplateFields(r.Context(), templateID, manifestID, patches); err != nil {
  275. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery(err.Error())), http.StatusSeeOther)
  276. return
  277. }
  278. http.Redirect(w, r, fmt.Sprintf("/templates/%d?msg=fields+saved", templateID), http.StatusSeeOther)
  279. }
  280. func (u *UI) BuildNew(w http.ResponseWriter, r *http.Request) {
  281. selectedTemplateID, _ := strconv.ParseInt(strings.TrimSpace(r.URL.Query().Get("template_id")), 10, 64)
  282. selectedDraftID := strings.TrimSpace(r.URL.Query().Get("draft_id"))
  283. form := buildFormInput{
  284. DraftID: selectedDraftID,
  285. DraftSource: "ui",
  286. DraftStatus: "draft",
  287. }
  288. fieldValues := map[string]string{}
  289. if selectedDraftID != "" {
  290. draft, err := u.draftSvc.GetDraft(r.Context(), selectedDraftID)
  291. if err == nil {
  292. selectedTemplateID = draft.TemplateID
  293. form = buildFormInputFromDraft(draft)
  294. fieldValues = parseFieldValuesJSON(draft.FieldValuesJSON)
  295. }
  296. }
  297. data, err := u.loadBuildNewPageData(r, basePageData(r, "New Build", "/builds/new"), selectedDraftID, selectedTemplateID, form, fieldValues)
  298. if err != nil {
  299. http.Error(w, err.Error(), http.StatusBadRequest)
  300. return
  301. }
  302. u.render.Render(w, "build_new", data)
  303. }
  304. func (u *UI) CreateBuild(w http.ResponseWriter, r *http.Request) {
  305. if err := r.ParseForm(); err != nil {
  306. http.Redirect(w, r, "/builds/new?err=invalid+form", http.StatusSeeOther)
  307. return
  308. }
  309. form := buildFormInputFromRequest(r)
  310. fieldValues := parseBuildFieldValues(r)
  311. globalData := buildsvc.BuildGlobalData(buildsvc.GlobalDataInput{
  312. CompanyName: form.CompanyName,
  313. BusinessType: form.BusinessType,
  314. Username: form.Username,
  315. Email: form.Email,
  316. Phone: form.Phone,
  317. OrgNumber: form.OrgNumber,
  318. StartDate: form.StartDate,
  319. Mission: form.Mission,
  320. DescriptionShort: form.DescriptionShort,
  321. DescriptionLong: form.DescriptionLong,
  322. SiteLanguage: form.SiteLanguage,
  323. AddressLine1: form.AddressLine1,
  324. AddressLine2: form.AddressLine2,
  325. AddressCity: form.AddressCity,
  326. AddressRegion: form.AddressRegion,
  327. AddressZIP: form.AddressZIP,
  328. AddressCountry: form.AddressCountry,
  329. })
  330. templateID, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("template_id")), 10, 64)
  331. if err != nil || templateID <= 0 {
  332. http.Redirect(w, r, "/builds/new?err=invalid+template", http.StatusSeeOther)
  333. return
  334. }
  335. result, err := u.buildSvc.StartBuild(r.Context(), buildsvc.StartBuildRequest{
  336. TemplateID: templateID,
  337. RequestName: form.RequestName,
  338. GlobalData: globalData,
  339. FieldValues: fieldValues,
  340. })
  341. if err != nil {
  342. data, loadErr := u.loadBuildNewPageData(r, pageData{
  343. Title: "New Build",
  344. Err: err.Error(),
  345. Current: "/builds/new",
  346. }, form.DraftID, templateID, form, fieldValues)
  347. if loadErr != nil {
  348. http.Error(w, loadErr.Error(), http.StatusBadRequest)
  349. return
  350. }
  351. u.render.Render(w, "build_new", data)
  352. return
  353. }
  354. if form.DraftID != "" {
  355. _, _ = u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  356. DraftID: form.DraftID,
  357. TemplateID: templateID,
  358. ManifestID: strings.TrimSpace(r.FormValue("manifest_id")),
  359. Source: form.DraftSource,
  360. RequestName: form.RequestName,
  361. GlobalData: globalData,
  362. FieldValues: fieldValues,
  363. Status: "submitted",
  364. Notes: form.DraftNotes,
  365. })
  366. }
  367. http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=build+started", result.BuildID), http.StatusSeeOther)
  368. }
  369. func (u *UI) SaveDraft(w http.ResponseWriter, r *http.Request) {
  370. if err := r.ParseForm(); err != nil {
  371. http.Redirect(w, r, "/builds/new?err=invalid+form", http.StatusSeeOther)
  372. return
  373. }
  374. form := buildFormInputFromRequest(r)
  375. fieldValues := parseBuildFieldValues(r)
  376. templateID, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("template_id")), 10, 64)
  377. if err != nil || templateID <= 0 {
  378. http.Redirect(w, r, "/builds/new?err=invalid+template", http.StatusSeeOther)
  379. return
  380. }
  381. globalData := buildsvc.BuildGlobalData(buildsvc.GlobalDataInput{
  382. CompanyName: form.CompanyName,
  383. BusinessType: form.BusinessType,
  384. Username: form.Username,
  385. Email: form.Email,
  386. Phone: form.Phone,
  387. OrgNumber: form.OrgNumber,
  388. StartDate: form.StartDate,
  389. Mission: form.Mission,
  390. DescriptionShort: form.DescriptionShort,
  391. DescriptionLong: form.DescriptionLong,
  392. SiteLanguage: form.SiteLanguage,
  393. AddressLine1: form.AddressLine1,
  394. AddressLine2: form.AddressLine2,
  395. AddressCity: form.AddressCity,
  396. AddressRegion: form.AddressRegion,
  397. AddressZIP: form.AddressZIP,
  398. AddressCountry: form.AddressCountry,
  399. })
  400. draft, err := u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  401. DraftID: form.DraftID,
  402. TemplateID: templateID,
  403. ManifestID: strings.TrimSpace(r.FormValue("manifest_id")),
  404. Source: form.DraftSource,
  405. RequestName: form.RequestName,
  406. GlobalData: globalData,
  407. FieldValues: fieldValues,
  408. Status: defaultDraftStatus(form.DraftStatus),
  409. Notes: form.DraftNotes,
  410. })
  411. if err != nil {
  412. data, loadErr := u.loadBuildNewPageData(r, pageData{
  413. Title: "New Build",
  414. Err: err.Error(),
  415. Current: "/builds/new",
  416. }, form.DraftID, templateID, form, fieldValues)
  417. if loadErr != nil {
  418. http.Error(w, loadErr.Error(), http.StatusBadRequest)
  419. return
  420. }
  421. u.render.Render(w, "build_new", data)
  422. return
  423. }
  424. http.Redirect(w, r, fmt.Sprintf("/builds/new?template_id=%d&draft_id=%s&msg=draft+saved", templateID, urlQuery(draft.ID)), http.StatusSeeOther)
  425. }
  426. func (u *UI) BuildDetail(w http.ResponseWriter, r *http.Request) {
  427. buildID := strings.TrimSpace(chi.URLParam(r, "id"))
  428. build, err := u.buildSvc.GetBuild(r.Context(), buildID)
  429. if err != nil {
  430. http.Error(w, err.Error(), http.StatusNotFound)
  431. return
  432. }
  433. status := strings.ToLower(strings.TrimSpace(build.QCStatus))
  434. canPoll := status == "queued" || status == "processing"
  435. canFetchEditor := (status == "done" || status == "failed" || status == "timeout") &&
  436. build.QCSiteID != nil &&
  437. strings.TrimSpace(build.QCEditorURL) == ""
  438. autoRefresh := 0
  439. if canPoll && u.cfg.PollIntervalSeconds > 0 {
  440. autoRefresh = u.cfg.PollIntervalSeconds
  441. }
  442. effectiveGlobal := build.GlobalDataJSON
  443. if payloadGlobal, err := extractGlobalDataFromFinalPayload(build.FinalSitesPayload); err == nil && len(payloadGlobal) > 0 {
  444. effectiveGlobal = payloadGlobal
  445. }
  446. u.render.Render(w, "build_detail", buildDetailPageData{
  447. pageData: basePageData(r, "Build Detail", "/builds"),
  448. Build: build,
  449. EffectiveGlobal: effectiveGlobal,
  450. CanPoll: canPoll,
  451. CanFetchEditorURL: canFetchEditor,
  452. AutoRefreshSeconds: autoRefresh,
  453. })
  454. }
  455. func (u *UI) PollBuild(w http.ResponseWriter, r *http.Request) {
  456. buildID := strings.TrimSpace(chi.URLParam(r, "id"))
  457. if err := u.buildSvc.PollOnce(r.Context(), buildID); err != nil {
  458. http.Redirect(w, r, fmt.Sprintf("/builds/%s?err=%s", buildID, urlQuery(err.Error())), http.StatusSeeOther)
  459. return
  460. }
  461. http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=poll+done", buildID), http.StatusSeeOther)
  462. }
  463. func (u *UI) FetchEditorURL(w http.ResponseWriter, r *http.Request) {
  464. buildID := strings.TrimSpace(chi.URLParam(r, "id"))
  465. if err := u.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil {
  466. http.Redirect(w, r, fmt.Sprintf("/builds/%s?err=%s", buildID, urlQuery(err.Error())), http.StatusSeeOther)
  467. return
  468. }
  469. http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=editor+url+loaded", buildID), http.StatusSeeOther)
  470. }
  471. func basePageData(r *http.Request, title, current string) pageData {
  472. q := r.URL.Query()
  473. return pageData{Title: title, Msg: q.Get("msg"), Err: q.Get("err"), Current: current}
  474. }
  475. func parseTemplateID(w http.ResponseWriter, r *http.Request) (int64, bool) {
  476. rawID := chi.URLParam(r, "id")
  477. templateID, err := strconv.ParseInt(rawID, 10, 64)
  478. if err != nil {
  479. http.Error(w, "invalid template id", http.StatusBadRequest)
  480. return 0, false
  481. }
  482. return templateID, true
  483. }
  484. func urlQuery(s string) string {
  485. return url.QueryEscape(s)
  486. }
  487. func boolPtr(v bool) *bool { return &v }
  488. func intPtr(v int) *int { return &v }
  489. func strPtr(v string) *string {
  490. return &v
  491. }
  492. func (u *UI) loadBuildNewPageData(r *http.Request, page pageData, selectedDraftID string, selectedTemplateID int64, form buildFormInput, fieldValues map[string]string) (buildNewPageData, error) {
  493. templates, err := u.templateSvc.ListTemplates(r.Context())
  494. if err != nil {
  495. return buildNewPageData{}, err
  496. }
  497. drafts, err := u.draftSvc.ListDrafts(r.Context(), 50)
  498. if err != nil {
  499. return buildNewPageData{}, err
  500. }
  501. data := buildNewPageData{
  502. pageData: page,
  503. Templates: templates,
  504. Drafts: drafts,
  505. SelectedDraftID: selectedDraftID,
  506. SelectedTemplateID: selectedTemplateID,
  507. Form: form,
  508. }
  509. if selectedTemplateID <= 0 {
  510. return data, nil
  511. }
  512. detail, err := u.templateSvc.GetTemplateDetail(r.Context(), selectedTemplateID)
  513. if err != nil || detail.Manifest == nil {
  514. return data, nil
  515. }
  516. data.SelectedManifestID = detail.Manifest.ID
  517. data.EditableFields, data.FieldSections = buildFieldSections(detail.Fields, fieldValues)
  518. data.EnabledFields = data.EditableFields
  519. return data, nil
  520. }
  521. func buildFieldSections(fields []domain.TemplateField, fieldValues map[string]string) ([]buildFieldView, []buildFieldSectionView) {
  522. sectionOrder := []string{
  523. domain.WebsiteSectionHero,
  524. domain.WebsiteSectionIntro,
  525. domain.WebsiteSectionServices,
  526. domain.WebsiteSectionAbout,
  527. domain.WebsiteSectionTeam,
  528. domain.WebsiteSectionTestimonials,
  529. domain.WebsiteSectionCTA,
  530. domain.WebsiteSectionContact,
  531. domain.WebsiteSectionGallery,
  532. domain.WebsiteSectionFooter,
  533. domain.WebsiteSectionOther,
  534. }
  535. sectionDescriptions := map[string]string{
  536. domain.WebsiteSectionHero: "Headline-nahe Felder, bevorzugt nach Block-ID gruppiert.",
  537. domain.WebsiteSectionIntro: "Intro-/Einleitungs-Felder, bevorzugt nach Block-ID gruppiert.",
  538. domain.WebsiteSectionServices: "Services-Felder, bevorzugt nach Block-ID gruppiert.",
  539. domain.WebsiteSectionAbout: "About-Felder, bevorzugt nach Block-ID gruppiert.",
  540. domain.WebsiteSectionTeam: "Team-Felder, bevorzugt nach Block-ID gruppiert.",
  541. domain.WebsiteSectionTestimonials: "Testimonial-Felder, bevorzugt nach Block-ID gruppiert.",
  542. domain.WebsiteSectionCTA: "CTA-/Highlight-Felder, bevorzugt nach Block-ID gruppiert.",
  543. domain.WebsiteSectionContact: "Kontakt-Felder, bevorzugt nach Block-ID gruppiert.",
  544. domain.WebsiteSectionGallery: "Media/Gallery-Felder, Bildfelder bleiben im MVP nicht editierbar.",
  545. domain.WebsiteSectionFooter: "Footer-Felder, bevorzugt nach Block-ID gruppiert.",
  546. domain.WebsiteSectionOther: "Aktive Textfelder ausserhalb der Kern-Sections, bevorzugt nach Block-ID gruppiert.",
  547. }
  548. sectionsByKey := make(map[string]buildFieldSectionView, len(sectionOrder))
  549. pendingByKey := make(map[string][]pendingField, len(sectionOrder))
  550. for _, key := range sectionOrder {
  551. sectionsByKey[key] = buildFieldSectionView{
  552. Key: key,
  553. Title: domain.WebsiteSectionLabel(key),
  554. Description: sectionDescriptions[key],
  555. }
  556. }
  557. for _, f := range fields {
  558. targetSection := preferredBuildSection(f)
  559. if isMediaOrGalleryField(f) || targetSection == domain.WebsiteSectionGallery {
  560. labelFallback := domain.WebsiteSectionLabel(domain.WebsiteSectionGallery) + " - " + humanizeKey(f.KeyName)
  561. if blockID := extractBlockID(f); blockID != "" {
  562. labelFallback = "Media - " + blockGroupTitle(blockID)
  563. }
  564. media := sectionsByKey[domain.WebsiteSectionGallery]
  565. media.DisabledFields = append(media.DisabledFields, buildFieldView{
  566. Path: f.Path,
  567. DisplayLabel: effectiveLabel(f, labelFallback),
  568. SampleValue: f.SampleValue,
  569. Value: "",
  570. })
  571. sectionsByKey[domain.WebsiteSectionGallery] = media
  572. continue
  573. }
  574. if !f.IsEnabled || !strings.EqualFold(strings.TrimSpace(f.FieldKind), "text") {
  575. continue
  576. }
  577. pf := pendingField{
  578. Field: f,
  579. View: buildFieldView{
  580. Path: f.Path,
  581. DisplayLabel: effectiveLabel(f, humanizeKey(f.KeyName)),
  582. SampleValue: f.SampleValue,
  583. Value: strings.TrimSpace(fieldValues[f.Path]),
  584. },
  585. }
  586. pendingByKey[targetSection] = append(pendingByKey[targetSection], pf)
  587. }
  588. for _, key := range sectionOrder {
  589. section := sectionsByKey[key]
  590. items := pendingByKey[key]
  591. switch key {
  592. case domain.WebsiteSectionServices:
  593. section = applyServicesGrouping(section, items)
  594. case domain.WebsiteSectionTestimonials:
  595. section = applyTestimonialsGrouping(section, items)
  596. case domain.WebsiteSectionHero, domain.WebsiteSectionIntro, domain.WebsiteSectionAbout, domain.WebsiteSectionTeam, domain.WebsiteSectionCTA, domain.WebsiteSectionContact, domain.WebsiteSectionFooter:
  597. section = applyTextGrouping(section, items)
  598. case domain.WebsiteSectionOther:
  599. section = applyOtherGrouping(section, items)
  600. case domain.WebsiteSectionGallery:
  601. // Gallery fields are handled as disabled entries only in this MVP.
  602. default:
  603. section = applyOtherGrouping(section, items)
  604. }
  605. sectionsByKey[key] = section
  606. }
  607. sections := make([]buildFieldSectionView, 0, len(sectionOrder))
  608. for _, key := range sectionOrder {
  609. sections = append(sections, sectionsByKey[key])
  610. }
  611. return assignEditableIndexes(sections)
  612. }
  613. func applyServicesGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  614. return applyBlockFirstGrouping(section, fields, "Services", applyServicesGroupingFallback)
  615. }
  616. func applyServicesGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  617. titles := make([]pendingField, 0)
  618. descriptions := make([]pendingField, 0)
  619. other := make([]buildFieldView, 0)
  620. for _, pf := range fields {
  621. key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName))
  622. switch {
  623. case strings.HasPrefix(key, "servicestitle_") || strings.HasPrefix(key, "servicestitle"):
  624. titles = append(titles, pf)
  625. case strings.HasPrefix(key, "servicesdescription_") || strings.HasPrefix(key, "servicesdescription"):
  626. descriptions = append(descriptions, pf)
  627. default:
  628. pf.View.DisplayLabel = effectiveLabel(pf.Field, "Services - "+humanizeKey(pf.Field.KeyName))
  629. other = append(other, pf.View)
  630. }
  631. }
  632. maxCount := len(titles)
  633. if len(descriptions) > maxCount {
  634. maxCount = len(descriptions)
  635. }
  636. for i := 0; i < maxCount; i++ {
  637. block := buildFieldGroupView{Title: fmt.Sprintf("Service %d", i+1)}
  638. if i < len(titles) {
  639. item := titles[i]
  640. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Service %d - Titel", i+1))
  641. block.Fields = append(block.Fields, item.View)
  642. }
  643. if i < len(descriptions) {
  644. item := descriptions[i]
  645. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Service %d - Beschreibung", i+1))
  646. block.Fields = append(block.Fields, item.View)
  647. }
  648. if len(block.Fields) > 0 {
  649. section.EditableGroups = append(section.EditableGroups, block)
  650. }
  651. }
  652. section.EditableFields = append(section.EditableFields, other...)
  653. return section
  654. }
  655. func applyTestimonialsGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  656. return applyBlockFirstGrouping(section, fields, "Testimonials", applyTestimonialsGroupingFallback)
  657. }
  658. func applyTestimonialsGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  659. names := make([]pendingField, 0)
  660. titles := make([]pendingField, 0)
  661. descriptions := make([]pendingField, 0)
  662. other := make([]buildFieldView, 0)
  663. for _, pf := range fields {
  664. key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName))
  665. switch {
  666. case strings.HasPrefix(key, "testimonialsname_") || strings.HasPrefix(key, "testimonialsname"):
  667. names = append(names, pf)
  668. case strings.HasPrefix(key, "testimonialstitle_") || strings.HasPrefix(key, "testimonialstitle"):
  669. titles = append(titles, pf)
  670. case strings.HasPrefix(key, "testimonialsdescription_") || strings.HasPrefix(key, "testimonialsdescription"):
  671. descriptions = append(descriptions, pf)
  672. default:
  673. pf.View.DisplayLabel = effectiveLabel(pf.Field, "Testimonials - "+humanizeKey(pf.Field.KeyName))
  674. other = append(other, pf.View)
  675. }
  676. }
  677. maxCount := len(names)
  678. if len(titles) > maxCount {
  679. maxCount = len(titles)
  680. }
  681. if len(descriptions) > maxCount {
  682. maxCount = len(descriptions)
  683. }
  684. for i := 0; i < maxCount; i++ {
  685. block := buildFieldGroupView{Title: fmt.Sprintf("Testimonial %d", i+1)}
  686. if i < len(names) {
  687. item := names[i]
  688. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Name", i+1))
  689. block.Fields = append(block.Fields, item.View)
  690. }
  691. if i < len(titles) {
  692. item := titles[i]
  693. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Titel", i+1))
  694. block.Fields = append(block.Fields, item.View)
  695. }
  696. if i < len(descriptions) {
  697. item := descriptions[i]
  698. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Beschreibung", i+1))
  699. block.Fields = append(block.Fields, item.View)
  700. }
  701. if len(block.Fields) > 0 {
  702. section.EditableGroups = append(section.EditableGroups, block)
  703. }
  704. }
  705. section.EditableFields = append(section.EditableFields, other...)
  706. return section
  707. }
  708. func applyTextGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  709. return applyBlockFirstGrouping(section, fields, "Text", applyTextGroupingFallback)
  710. }
  711. func applyTextGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  712. titles := make([]pendingField, 0)
  713. descriptions := make([]pendingField, 0)
  714. names := make([]pendingField, 0)
  715. other := make([]buildFieldView, 0)
  716. for _, pf := range fields {
  717. key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName))
  718. switch {
  719. case strings.HasPrefix(key, "texttitle_") || strings.HasPrefix(key, "texttitle") || strings.HasPrefix(key, "exttitle_") || strings.HasPrefix(key, "exttitle"):
  720. titles = append(titles, pf)
  721. case strings.HasPrefix(key, "textdescription_") || strings.HasPrefix(key, "textdescription") || strings.HasPrefix(key, "extdescription_") || strings.HasPrefix(key, "extdescription"):
  722. descriptions = append(descriptions, pf)
  723. case strings.HasPrefix(key, "textname_") || strings.HasPrefix(key, "textname") || strings.HasPrefix(key, "extname_") || strings.HasPrefix(key, "extname"):
  724. names = append(names, pf)
  725. default:
  726. pf.View.DisplayLabel = effectiveLabel(pf.Field, "Text - "+humanizeKey(pf.Field.KeyName))
  727. other = append(other, pf.View)
  728. }
  729. }
  730. maxCount := len(titles)
  731. if len(descriptions) > maxCount {
  732. maxCount = len(descriptions)
  733. }
  734. if len(names) > maxCount {
  735. maxCount = len(names)
  736. }
  737. for i := 0; i < maxCount; i++ {
  738. block := buildFieldGroupView{Title: fmt.Sprintf("Textblock %d", i+1)}
  739. if i < len(titles) {
  740. item := titles[i]
  741. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Titel", i+1))
  742. block.Fields = append(block.Fields, item.View)
  743. }
  744. if i < len(descriptions) {
  745. item := descriptions[i]
  746. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Beschreibung", i+1))
  747. block.Fields = append(block.Fields, item.View)
  748. }
  749. if i < len(names) {
  750. item := names[i]
  751. item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Name", i+1))
  752. block.Fields = append(block.Fields, item.View)
  753. }
  754. if len(block.Fields) > 0 {
  755. section.EditableGroups = append(section.EditableGroups, block)
  756. }
  757. }
  758. section.EditableFields = append(section.EditableFields, other...)
  759. return section
  760. }
  761. func applyOtherGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  762. return applyBlockFirstGrouping(section, fields, "", applyOtherGroupingFallback)
  763. }
  764. func applyOtherGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
  765. for _, pf := range fields {
  766. pf.View.DisplayLabel = effectiveLabel(pf.Field, normalizedSectionTitle(pf.Field.Section)+" - "+humanizeKey(pf.Field.KeyName))
  767. section.EditableFields = append(section.EditableFields, pf.View)
  768. }
  769. sort.SliceStable(section.EditableFields, func(i, j int) bool {
  770. return section.EditableFields[i].Path < section.EditableFields[j].Path
  771. })
  772. return section
  773. }
  774. func applyBlockFirstGrouping(section buildFieldSectionView, fields []pendingField, fallbackPrefix string, fallback func(buildFieldSectionView, []pendingField) buildFieldSectionView) buildFieldSectionView {
  775. grouped := map[string][]pendingField{}
  776. withoutBlockID := make([]pendingField, 0)
  777. for _, pf := range fields {
  778. blockID := extractBlockID(pf.Field)
  779. if blockID == "" {
  780. withoutBlockID = append(withoutBlockID, pf)
  781. continue
  782. }
  783. grouped[blockID] = append(grouped[blockID], pf)
  784. }
  785. if len(grouped) > 0 {
  786. blockIDs := make([]string, 0, len(grouped))
  787. for blockID := range grouped {
  788. blockIDs = append(blockIDs, blockID)
  789. }
  790. sort.SliceStable(blockIDs, func(i, j int) bool {
  791. li, lj := blockSortRank(blockIDs[i]), blockSortRank(blockIDs[j])
  792. if li != lj {
  793. return li < lj
  794. }
  795. return blockIDs[i] < blockIDs[j]
  796. })
  797. for _, blockID := range blockIDs {
  798. items := grouped[blockID]
  799. sort.SliceStable(items, func(i, j int) bool {
  800. ri := deriveFieldRole(items[i].Field.KeyName)
  801. rj := deriveFieldRole(items[j].Field.KeyName)
  802. if ri.Order != rj.Order {
  803. return ri.Order < rj.Order
  804. }
  805. return items[i].Field.Path < items[j].Field.Path
  806. })
  807. group := buildFieldGroupView{Title: blockGroupTitle(blockID)}
  808. for _, item := range items {
  809. role := deriveFieldRole(item.Field.KeyName)
  810. fallbackLabel := role.Label
  811. if fallbackLabel == "" {
  812. fallbackLabel = humanizeKey(item.Field.KeyName)
  813. }
  814. item.View.DisplayLabel = effectiveLabel(item.Field, fallbackLabel)
  815. group.Fields = append(group.Fields, item.View)
  816. }
  817. if len(group.Fields) > 0 {
  818. section.EditableGroups = append(section.EditableGroups, group)
  819. }
  820. }
  821. }
  822. if len(withoutBlockID) > 0 {
  823. if fallback != nil {
  824. return fallback(section, withoutBlockID)
  825. }
  826. for _, pf := range withoutBlockID {
  827. labelPrefix := fallbackPrefix
  828. if labelPrefix == "" {
  829. labelPrefix = normalizedSectionTitle(pf.Field.Section)
  830. }
  831. pf.View.DisplayLabel = effectiveLabel(pf.Field, labelPrefix+" - "+humanizeKey(pf.Field.KeyName))
  832. section.EditableFields = append(section.EditableFields, pf.View)
  833. }
  834. }
  835. return section
  836. }
  837. func extractBlockID(f domain.TemplateField) string {
  838. candidates := []string{f.KeyName, f.Path}
  839. for _, candidate := range candidates {
  840. normalized := strings.ToLower(strings.TrimSpace(candidate))
  841. if normalized == "" {
  842. continue
  843. }
  844. if match := blockIDPattern.FindStringSubmatch(normalized); len(match) > 1 {
  845. return match[1]
  846. }
  847. if match := looseBlockIDPattern.FindStringSubmatch(normalized); len(match) > 1 {
  848. return match[1]
  849. }
  850. }
  851. return ""
  852. }
  853. func blockGroupTitle(blockID string) string {
  854. blockID = strings.ToLower(strings.TrimSpace(blockID))
  855. if blockID == "" {
  856. return "Unbekannter Block"
  857. }
  858. if known, ok := knownBlockAreas[blockID]; ok {
  859. return fmt.Sprintf("%s (%s)", known, blockID)
  860. }
  861. return "Block " + blockID
  862. }
  863. func blockSortRank(blockID string) int {
  864. switch strings.ToLower(strings.TrimSpace(blockID)) {
  865. case "m1710":
  866. return 10
  867. case "c7886":
  868. return 20
  869. case "r4830":
  870. return 30
  871. case "c2929":
  872. return 40
  873. case "r4748":
  874. return 50
  875. case "r1508":
  876. return 60
  877. case "c1165":
  878. return 70
  879. case "m4178":
  880. return 80
  881. default:
  882. return 1000
  883. }
  884. }
  885. func deriveFieldRole(key string) fieldRole {
  886. normalized := strings.ToLower(strings.TrimSpace(key))
  887. switch {
  888. case strings.Contains(normalized, "subtitle"):
  889. return fieldRole{Label: "Untertitel", Order: 15}
  890. case strings.Contains(normalized, "title"):
  891. return fieldRole{Label: "Titel", Order: 20}
  892. case strings.Contains(normalized, "description"):
  893. return fieldRole{Label: "Beschreibung", Order: 30}
  894. case strings.Contains(normalized, "name"):
  895. return fieldRole{Label: "Name", Order: 40}
  896. case strings.Contains(normalized, "button") || strings.Contains(normalized, "cta"):
  897. return fieldRole{Label: "CTA Text", Order: 50}
  898. default:
  899. return fieldRole{Label: humanizeKey(key), Order: 100}
  900. }
  901. }
  902. func assignEditableIndexes(sections []buildFieldSectionView) ([]buildFieldView, []buildFieldSectionView) {
  903. editable := make([]buildFieldView, 0)
  904. nextIndex := 0
  905. for si := range sections {
  906. for gi := range sections[si].EditableGroups {
  907. for fi := range sections[si].EditableGroups[gi].Fields {
  908. sections[si].EditableGroups[gi].Fields[fi].Index = nextIndex
  909. editable = append(editable, sections[si].EditableGroups[gi].Fields[fi])
  910. nextIndex++
  911. }
  912. }
  913. for fi := range sections[si].EditableFields {
  914. sections[si].EditableFields[fi].Index = nextIndex
  915. editable = append(editable, sections[si].EditableFields[fi])
  916. nextIndex++
  917. }
  918. }
  919. return editable, sections
  920. }
  921. func preferredBuildSection(f domain.TemplateField) string {
  922. websiteSection := strings.TrimSpace(f.WebsiteSection)
  923. if websiteSection != "" {
  924. normalized := domain.NormalizeWebsiteSection(websiteSection)
  925. if normalized == domain.WebsiteSectionServiceItem {
  926. return domain.WebsiteSectionServices
  927. }
  928. return normalized
  929. }
  930. return fallbackBuildSection(f)
  931. }
  932. func fallbackBuildSection(f domain.TemplateField) string {
  933. switch normalizedSection(f.Section) {
  934. case "services":
  935. return domain.WebsiteSectionServices
  936. case "testimonials":
  937. return domain.WebsiteSectionTestimonials
  938. case "text":
  939. if isMediaOrGalleryField(f) {
  940. return domain.WebsiteSectionGallery
  941. }
  942. return domain.WebsiteSectionOther
  943. default:
  944. if isMediaOrGalleryField(f) {
  945. return domain.WebsiteSectionGallery
  946. }
  947. return domain.WebsiteSectionOther
  948. }
  949. }
  950. func normalizedSection(raw string) string {
  951. section := strings.ToLower(strings.TrimSpace(raw))
  952. switch section {
  953. case "ext":
  954. return "text"
  955. default:
  956. return section
  957. }
  958. }
  959. func normalizedSectionTitle(raw string) string {
  960. switch normalizedSection(raw) {
  961. case "text":
  962. return "Text"
  963. case "services":
  964. return "Services"
  965. case "testimonials":
  966. return "Testimonials"
  967. case "gallery", "media":
  968. return "Media"
  969. default:
  970. return "Feld"
  971. }
  972. }
  973. func isMediaOrGalleryField(f domain.TemplateField) bool {
  974. if strings.EqualFold(strings.TrimSpace(f.FieldKind), "image") {
  975. return true
  976. }
  977. section := strings.ToLower(strings.TrimSpace(f.Section))
  978. key := strings.ToLower(strings.TrimSpace(f.KeyName))
  979. path := strings.ToLower(strings.TrimSpace(f.Path))
  980. if section == "gallery" || section == "media" {
  981. return true
  982. }
  983. hints := []string{"gallery", "image", "img", "photo", "picture"}
  984. for _, hint := range hints {
  985. if strings.Contains(section, hint) || strings.Contains(key, hint) || strings.Contains(path, hint) {
  986. return true
  987. }
  988. }
  989. return false
  990. }
  991. func effectiveLabel(f domain.TemplateField, fallback string) string {
  992. if !isRawPathLikeLabel(f.DisplayLabel, f.Path) {
  993. return strings.TrimSpace(f.DisplayLabel)
  994. }
  995. return strings.TrimSpace(fallback)
  996. }
  997. func isRawPathLikeLabel(label string, path string) bool {
  998. l := strings.TrimSpace(label)
  999. if l == "" {
  1000. return true
  1001. }
  1002. if strings.EqualFold(l, strings.TrimSpace(path)) {
  1003. return true
  1004. }
  1005. if strings.Contains(l, ".") || strings.Contains(l, "_") {
  1006. return true
  1007. }
  1008. return false
  1009. }
  1010. func humanizeKey(key string) string {
  1011. raw := strings.TrimSpace(key)
  1012. if raw == "" {
  1013. return "Inhalt"
  1014. }
  1015. base := raw
  1016. if idx := strings.Index(base, "_"); idx > 0 {
  1017. base = base[:idx]
  1018. }
  1019. runes := make([]rune, 0, len(base)+4)
  1020. for i, r := range base {
  1021. if i > 0 && unicode.IsUpper(r) {
  1022. runes = append(runes, ' ')
  1023. }
  1024. runes = append(runes, r)
  1025. }
  1026. human := strings.TrimSpace(string(runes))
  1027. if human == "" {
  1028. return "Inhalt"
  1029. }
  1030. words := strings.Fields(strings.ToLower(human))
  1031. for i := range words {
  1032. if len(words[i]) > 0 {
  1033. words[i] = strings.ToUpper(words[i][:1]) + words[i][1:]
  1034. }
  1035. }
  1036. return strings.Join(words, " ")
  1037. }
  1038. func buildFormInputFromRequest(r *http.Request) buildFormInput {
  1039. return buildFormInput{
  1040. DraftID: strings.TrimSpace(r.FormValue("draft_id")),
  1041. DraftSource: strings.TrimSpace(r.FormValue("draft_source")),
  1042. DraftStatus: strings.TrimSpace(r.FormValue("draft_status")),
  1043. DraftNotes: strings.TrimSpace(r.FormValue("draft_notes")),
  1044. RequestName: strings.TrimSpace(r.FormValue("request_name")),
  1045. CompanyName: strings.TrimSpace(r.FormValue("company_name")),
  1046. BusinessType: strings.TrimSpace(r.FormValue("business_type")),
  1047. Username: strings.TrimSpace(r.FormValue("username")),
  1048. Email: strings.TrimSpace(r.FormValue("email")),
  1049. Phone: strings.TrimSpace(r.FormValue("phone")),
  1050. OrgNumber: strings.TrimSpace(r.FormValue("org_number")),
  1051. StartDate: strings.TrimSpace(r.FormValue("start_date")),
  1052. Mission: strings.TrimSpace(r.FormValue("mission")),
  1053. DescriptionShort: strings.TrimSpace(r.FormValue("description_short")),
  1054. DescriptionLong: strings.TrimSpace(r.FormValue("description_long")),
  1055. SiteLanguage: strings.TrimSpace(r.FormValue("site_language")),
  1056. AddressLine1: strings.TrimSpace(r.FormValue("address_line1")),
  1057. AddressLine2: strings.TrimSpace(r.FormValue("address_line2")),
  1058. AddressCity: strings.TrimSpace(r.FormValue("address_city")),
  1059. AddressRegion: strings.TrimSpace(r.FormValue("address_region")),
  1060. AddressZIP: strings.TrimSpace(r.FormValue("address_zip")),
  1061. AddressCountry: strings.TrimSpace(r.FormValue("address_country")),
  1062. }
  1063. }
  1064. func buildFormInputFromDraft(draft *domain.BuildDraft) buildFormInput {
  1065. form := buildFormInput{
  1066. DraftID: draft.ID,
  1067. DraftSource: draft.Source,
  1068. DraftStatus: draft.Status,
  1069. DraftNotes: draft.Notes,
  1070. RequestName: draft.RequestName,
  1071. }
  1072. mergeGlobalDataIntoForm(&form, draft.GlobalDataJSON)
  1073. return form
  1074. }
  1075. func parseBuildFieldValues(r *http.Request) map[string]string {
  1076. fieldValues := map[string]string{}
  1077. count, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("field_count")))
  1078. for i := 0; i < count; i++ {
  1079. path := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_path_%d", i)))
  1080. value := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_value_%d", i)))
  1081. if path != "" {
  1082. fieldValues[path] = value
  1083. }
  1084. }
  1085. return fieldValues
  1086. }
  1087. func extractGlobalDataFromFinalPayload(raw []byte) ([]byte, error) {
  1088. if len(raw) == 0 {
  1089. return nil, nil
  1090. }
  1091. var payload struct {
  1092. GlobalData map[string]any `json:"globalData"`
  1093. }
  1094. if err := json.Unmarshal(raw, &payload); err != nil {
  1095. return nil, err
  1096. }
  1097. if len(payload.GlobalData) == 0 {
  1098. return nil, nil
  1099. }
  1100. data, err := json.Marshal(payload.GlobalData)
  1101. if err != nil {
  1102. return nil, err
  1103. }
  1104. return data, nil
  1105. }
  1106. func parseFieldValuesJSON(raw []byte) map[string]string {
  1107. out := map[string]string{}
  1108. if len(raw) == 0 {
  1109. return out
  1110. }
  1111. _ = json.Unmarshal(raw, &out)
  1112. return out
  1113. }
  1114. func mergeGlobalDataIntoForm(form *buildFormInput, raw []byte) {
  1115. if form == nil || len(raw) == 0 {
  1116. return
  1117. }
  1118. var global map[string]any
  1119. if err := json.Unmarshal(raw, &global); err != nil {
  1120. return
  1121. }
  1122. form.CompanyName = getString(global["companyName"])
  1123. form.BusinessType = getString(global["businessType"])
  1124. form.Username = getString(global["username"])
  1125. form.Email = getString(global["email"])
  1126. form.Phone = getString(global["phone"])
  1127. form.OrgNumber = getString(global["orgNumber"])
  1128. form.StartDate = getString(global["startDate"])
  1129. form.Mission = getString(global["mission"])
  1130. form.DescriptionShort = getString(global["descriptionShort"])
  1131. form.DescriptionLong = getString(global["descriptionLong"])
  1132. form.SiteLanguage = getString(global["siteLanguage"])
  1133. address, _ := global["address"].(map[string]any)
  1134. form.AddressLine1 = getString(address["line1"])
  1135. form.AddressLine2 = getString(address["line2"])
  1136. form.AddressCity = getString(address["city"])
  1137. form.AddressRegion = getString(address["region"])
  1138. form.AddressZIP = getString(address["zip"])
  1139. form.AddressCountry = getString(address["country"])
  1140. }
  1141. func getString(v any) string {
  1142. s, _ := v.(string)
  1143. return strings.TrimSpace(s)
  1144. }
  1145. func defaultDraftStatus(status string) string {
  1146. switch strings.ToLower(strings.TrimSpace(status)) {
  1147. case "reviewed", "submitted":
  1148. return strings.ToLower(strings.TrimSpace(status))
  1149. default:
  1150. return "draft"
  1151. }
  1152. }
  1153. func websiteSectionOptions() []websiteSectionOptionView {
  1154. values := domain.WebsiteSectionOptions()
  1155. out := make([]websiteSectionOptionView, 0, len(values))
  1156. for _, value := range values {
  1157. out = append(out, websiteSectionOptionView{
  1158. Value: value,
  1159. Label: domain.WebsiteSectionLabel(value),
  1160. })
  1161. }
  1162. return out
  1163. }