mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
feat: WIP Contnet managemtn
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Editable customer-facing copy for the portal's public pages (/help, /faq,
|
||||
* /terms, /privacy) plus the shared support-contact block, with an append-only
|
||||
* version log behind it.
|
||||
*
|
||||
* `payload` is opaque jsonb: the five documents have genuinely different shapes
|
||||
* and the help page's blocks change with the copy, so typed columns would mean
|
||||
* a migration per wording tweak. The shape is enforced by per-slug DTOs on
|
||||
* write instead.
|
||||
*
|
||||
* No rows are inserted here — `SupportContentSeeder` fills the table on first
|
||||
* boot and skips whenever it is non-empty, so a redeploy never overwrites
|
||||
* admin edits the way a migration-embedded INSERT eventually would.
|
||||
*/
|
||||
export class SupportContent3350000000000 implements MigrationInterface {
|
||||
name = "SupportContent3350000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.support_documents (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
slug varchar(32) NOT NULL,
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
updated_by_id uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz
|
||||
);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_support_documents_slug
|
||||
ON freight.support_documents (slug);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.support_document_versions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
document_id uuid NOT NULL
|
||||
REFERENCES freight.support_documents(id) ON DELETE CASCADE,
|
||||
version integer NOT NULL,
|
||||
payload jsonb NOT NULL,
|
||||
actor_id uuid,
|
||||
note varchar(255),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz
|
||||
);
|
||||
`);
|
||||
|
||||
// Closes the concurrent-save race: two editors saving at once cannot both
|
||||
// claim the same version number.
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_support_doc_version
|
||||
ON freight.support_document_versions (document_id, version);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_support_doc_versions_document
|
||||
ON freight.support_document_versions (document_id);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP TABLE IF EXISTS freight.support_document_versions;`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.support_documents;`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Converts the HELP document from its original fixed-block shape
|
||||
* (`video` / `chat` / `channels` / `topics` / `checklist`) to the free-form
|
||||
* `sections[]` builder, where every block is a heading plus markdown plus
|
||||
* attached media.
|
||||
*
|
||||
* Only rows still in the old shape are touched — detected by the presence of a
|
||||
* `channels` key — so this is a no-op on any environment seeded after the
|
||||
* change, and re-running it does nothing.
|
||||
*
|
||||
* The payload literal is inlined rather than imported from
|
||||
* `SUPPORT_CONTENT_DEFAULTS`: a migration must keep doing the same thing
|
||||
* forever, and that constant will keep moving.
|
||||
*
|
||||
* The rewrite also bumps `version` and writes a matching history row. The live
|
||||
* row's version always having a matching entry in
|
||||
* `support_document_versions` is the invariant the history list and rollback
|
||||
* both depend on, and a silent payload swap would break it.
|
||||
*/
|
||||
const HELP_SECTIONS = [
|
||||
{
|
||||
id: "help-walkthrough",
|
||||
heading: "Portal walkthrough",
|
||||
body: "A guided tour of the portal — registering your company, raising a booking against a contract, and settling an invoice.",
|
||||
media: [
|
||||
{
|
||||
id: "help-walkthrough-video",
|
||||
kind: "video",
|
||||
src: "/assets/edr-portal-guide.webm",
|
||||
caption: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "help-chat",
|
||||
heading: "Chat with our team",
|
||||
body: "Signed-in customers can open a support conversation from the headset button at the bottom right of every portal page. You can send screenshots and documents in the chat, and replies appear there and as a notification.\n\n[Open the portal](/portal)",
|
||||
media: [],
|
||||
},
|
||||
{
|
||||
id: "help-contact",
|
||||
heading: "Contact us",
|
||||
body: "- **Email** — [{{supportEmail}}](mailto:{{supportEmail}}). Best for document issues and anything needing an attachment.\n- **Phone** — [{{supportPhone}}](tel:{{supportPhoneTel}}). Best for urgent problems with cargo already in transit.\n- **Head office** — {{supportOffice}}. Walk-in support during working hours.\n- **Support hours** — {{supportHours}}. Outside these hours, email us and we reply the next working day.",
|
||||
media: [],
|
||||
},
|
||||
{
|
||||
id: "help-topics",
|
||||
heading: "Common topics",
|
||||
body: "- **[Account & onboarding](/faq)** — registering your company, uploading your trade licence and TIN, and getting an operational profile approved.\n- **[Contracts](/faq)** — requesting a freight contract, reviewing its terms and signing it with your saved signature and stamp.\n- **[Bookings & tracking](/faq)** — raising a booking against a contract, adding last-mile transport and following the consignment along the corridor.\n- **[Invoices & payments](/faq)** — finding invoices, paying through the bank channels and confirming a payment that has not yet settled.",
|
||||
media: [],
|
||||
},
|
||||
{
|
||||
id: "help-checklist",
|
||||
heading: "What to include when you contact us",
|
||||
body: "- Your company name and the email you sign in with.\n- The reference of the contract, booking or invoice involved.\n- What you expected to happen and what happened instead.\n- A screenshot of any error message the portal showed.",
|
||||
media: [],
|
||||
},
|
||||
];
|
||||
|
||||
export class SupportHelpSections3360000000000 implements MigrationInterface {
|
||||
name = "SupportHelpSections3360000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const rows: { id: string; version: number; payload: Record<string, unknown> }[] =
|
||||
await queryRunner.query(`
|
||||
SELECT id, version, payload
|
||||
FROM freight.support_documents
|
||||
WHERE slug = 'HELP' AND payload ? 'channels'
|
||||
`);
|
||||
|
||||
for (const row of rows) {
|
||||
const payload = {
|
||||
title: row.payload.title ?? "Help & Support",
|
||||
subtitle:
|
||||
row.payload.subtitle ??
|
||||
"Get answers fast — watch the walkthrough, browse the common topics, check the FAQ, or reach our team directly.",
|
||||
sections: HELP_SECTIONS,
|
||||
};
|
||||
const version = row.version + 1;
|
||||
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.support_documents
|
||||
SET payload = $1::jsonb, version = $2, updated_at = now()
|
||||
WHERE id = $3`,
|
||||
[JSON.stringify(payload), version, row.id],
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO freight.support_document_versions
|
||||
(document_id, version, payload, actor_id, note)
|
||||
VALUES ($1, $2, $3::jsonb, NULL, $4)`,
|
||||
[
|
||||
row.id,
|
||||
version,
|
||||
JSON.stringify(payload),
|
||||
"Converted help page to free-form sections",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not reversible: the old fixed blocks cannot be recovered from markdown
|
||||
* sections an editor may since have rewritten. The version history holds the
|
||||
* pre-conversion payload if it is ever genuinely needed.
|
||||
*/
|
||||
public async down(): Promise<void> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user