feat: MyEasyCMS v2 — Full SaaS rebuild
Some checks failed
Workflow / ⚫️ Test (push) Has been cancelled
Workflow / ʦ TypeScript (push) Has been cancelled

Complete rebuild of 22-year-old PHP CMS as modern SaaS:

Database (15 migrations, 42+ tables):
- Foundation: account_settings, audit_log, GDPR register, cms_files
- Module Engine: modules, fields, records, permissions, relations + RPC
- Members: 45+ field member profiles, departments, roles, honors, SEPA mandates
- Courses: courses, sessions, categories, instructors, locations, attendance
- Bookings: rooms, guests, bookings with availability
- Events: events, registrations, holiday passes
- Finance: SEPA batches/items (pain.008/001 XML), invoices
- Newsletter: campaigns, templates, recipients, subscriptions
- Site Builder: site_pages (Puck JSON), site_settings, cms_posts
- Portal Auth: member_portal_invitations, user linking

Feature Packages (9):
- @kit/module-builder — dynamic low-code CRUD engine
- @kit/member-management — 31 API methods, 21 actions, 8 components
- @kit/course-management, @kit/booking-management, @kit/event-management
- @kit/finance — SEPA XML generator + IBAN validator
- @kit/newsletter — campaigns + dispatch
- @kit/document-generator — PDF/Excel/Word
- @kit/site-builder — Puck visual editor, 15 blocks, public rendering

Pages (60+):
- Dashboard with real stats from all APIs
- Full CRUD for all 8 domains with react-hook-form + Zod
- Recharts statistics
- German i18n throughout
- Member portal with auth + invitation system
- Public club websites via Puck at /club/[slug]

Infrastructure:
- Dockerfile (multi-stage, standalone output)
- docker-compose.yml (Supabase self-hosted + Next.js)
- Kong API gateway config
- .env.production.example
This commit is contained in:
Zaid Marzguioui
2026-03-29 23:17:38 +02:00
parent 61ff48cb73
commit 1294caa7fa
120 changed files with 11013 additions and 1858 deletions

View File

@@ -496,6 +496,73 @@ export type Database = {
},
]
}
cms_posts: {
Row: {
account_id: string
author_id: string | null
content: string | null
cover_image: string | null
created_at: string
excerpt: string | null
id: string
published_at: string | null
slug: string
status: string
title: string
updated_at: string
}
Insert: {
account_id: string
author_id?: string | null
content?: string | null
cover_image?: string | null
created_at?: string
excerpt?: string | null
id?: string
published_at?: string | null
slug: string
status?: string
title: string
updated_at?: string
}
Update: {
account_id?: string
author_id?: string | null
content?: string | null
cover_image?: string | null
created_at?: string
excerpt?: string | null
id?: string
published_at?: string | null
slug?: string
status?: string
title?: string
updated_at?: string
}
Relationships: [
{
foreignKeyName: "cms_posts_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "cms_posts_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "cms_posts_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
config: {
Row: {
billing_provider: Database["public"]["Enums"]["billing_provider"]
@@ -963,6 +1030,8 @@ export type Database = {
id: string
interval: string
is_default: boolean
is_exit: boolean
is_youth: boolean
name: string
sort_order: number
}
@@ -974,6 +1043,8 @@ export type Database = {
id?: string
interval?: string
is_default?: boolean
is_exit?: boolean
is_youth?: boolean
name: string
sort_order?: number
}
@@ -985,6 +1056,8 @@ export type Database = {
id?: string
interval?: string
is_default?: boolean
is_exit?: boolean
is_youth?: boolean
name?: string
sort_order?: number
}
@@ -1671,126 +1744,499 @@ export type Database = {
},
]
}
member_department_assignments: {
Row: {
department_id: string
member_id: string
}
Insert: {
department_id: string
member_id: string
}
Update: {
department_id?: string
member_id?: string
}
Relationships: [
{
foreignKeyName: "member_department_assignments_department_id_fkey"
columns: ["department_id"]
isOneToOne: false
referencedRelation: "member_departments"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_department_assignments_member_id_fkey"
columns: ["member_id"]
isOneToOne: false
referencedRelation: "members"
referencedColumns: ["id"]
},
]
}
member_departments: {
Row: {
account_id: string
created_at: string
description: string | null
id: string
name: string
sort_order: number
}
Insert: {
account_id: string
created_at?: string
description?: string | null
id?: string
name: string
sort_order?: number
}
Update: {
account_id?: string
created_at?: string
description?: string | null
id?: string
name?: string
sort_order?: number
}
Relationships: [
{
foreignKeyName: "member_departments_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_departments_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_departments_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
member_honors: {
Row: {
account_id: string
created_at: string
description: string | null
honor_date: string | null
honor_name: string
id: string
member_id: string
}
Insert: {
account_id: string
created_at?: string
description?: string | null
honor_date?: string | null
honor_name: string
id?: string
member_id: string
}
Update: {
account_id?: string
created_at?: string
description?: string | null
honor_date?: string | null
honor_name?: string
id?: string
member_id?: string
}
Relationships: [
{
foreignKeyName: "member_honors_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_honors_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_honors_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_honors_member_id_fkey"
columns: ["member_id"]
isOneToOne: false
referencedRelation: "members"
referencedColumns: ["id"]
},
]
}
member_portal_invitations: {
Row: {
accepted_at: string | null
account_id: string
created_at: string
email: string
expires_at: string
id: string
invite_token: string
invited_by: string | null
member_id: string
status: string
}
Insert: {
accepted_at?: string | null
account_id: string
created_at?: string
email: string
expires_at?: string
id?: string
invite_token?: string
invited_by?: string | null
member_id: string
status?: string
}
Update: {
accepted_at?: string | null
account_id?: string
created_at?: string
email?: string
expires_at?: string
id?: string
invite_token?: string
invited_by?: string | null
member_id?: string
status?: string
}
Relationships: [
{
foreignKeyName: "member_portal_invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_portal_invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_portal_invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_portal_invitations_member_id_fkey"
columns: ["member_id"]
isOneToOne: false
referencedRelation: "members"
referencedColumns: ["id"]
},
]
}
member_roles: {
Row: {
account_id: string
created_at: string
from_date: string | null
id: string
is_active: boolean
member_id: string
role_name: string
until_date: string | null
}
Insert: {
account_id: string
created_at?: string
from_date?: string | null
id?: string
is_active?: boolean
member_id: string
role_name: string
until_date?: string | null
}
Update: {
account_id?: string
created_at?: string
from_date?: string | null
id?: string
is_active?: boolean
member_id?: string
role_name?: string
until_date?: string | null
}
Relationships: [
{
foreignKeyName: "member_roles_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_roles_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_roles_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "member_roles_member_id_fkey"
columns: ["member_id"]
isOneToOne: false
referencedRelation: "members"
referencedColumns: ["id"]
},
]
}
members: {
Row: {
account_holder: string | null
account_id: string
additional_fees: number | null
address_invalid: boolean
bic: string | null
birth_country: string | null
birthplace: string | null
city: string | null
country: string | null
created_at: string
created_by: string | null
custom_data: Json
data_reconciliation_needed: boolean
date_of_birth: string | null
dues_category_id: string | null
dues_paid: boolean
dues_year: number | null
email: string | null
email_confirmed: boolean
entry_date: string
exemption_amount: number | null
exemption_reason: string | null
exemption_type: string | null
exit_date: string | null
exit_reason: string | null
fax: string | null
first_name: string
gdpr_birthday_info: boolean
gdpr_consent: boolean
gdpr_consent_date: string | null
gdpr_data_source: string | null
gdpr_internet: boolean
gdpr_newsletter: boolean
gdpr_print: boolean
gender: string | null
guardian_email: string | null
guardian_name: string | null
guardian_phone: string | null
house_number: string | null
iban: string | null
id: string
is_archived: boolean
is_founding_member: boolean
is_honorary: boolean
is_probationary: boolean
is_retiree: boolean
is_transferred: boolean
is_youth: boolean
last_name: string
member_number: string | null
mobile: string | null
notes: string | null
online_access_blocked: boolean
online_access_key: string | null
phone: string | null
phone2: string | null
postal_code: string | null
salutation: string | null
sepa_bank_name: string | null
sepa_mandate_date: string | null
sepa_mandate_id: string | null
sepa_mandate_reference: string | null
sepa_mandate_sequence: string | null
sepa_mandate_status:
| Database["public"]["Enums"]["sepa_mandate_status"]
| null
status: Database["public"]["Enums"]["membership_status"]
street: string | null
street2: string | null
title: string | null
updated_at: string
updated_by: string | null
user_id: string | null
}
Insert: {
account_holder?: string | null
account_id: string
additional_fees?: number | null
address_invalid?: boolean
bic?: string | null
birth_country?: string | null
birthplace?: string | null
city?: string | null
country?: string | null
created_at?: string
created_by?: string | null
custom_data?: Json
data_reconciliation_needed?: boolean
date_of_birth?: string | null
dues_category_id?: string | null
dues_paid?: boolean
dues_year?: number | null
email?: string | null
email_confirmed?: boolean
entry_date?: string
exemption_amount?: number | null
exemption_reason?: string | null
exemption_type?: string | null
exit_date?: string | null
exit_reason?: string | null
fax?: string | null
first_name: string
gdpr_birthday_info?: boolean
gdpr_consent?: boolean
gdpr_consent_date?: string | null
gdpr_data_source?: string | null
gdpr_internet?: boolean
gdpr_newsletter?: boolean
gdpr_print?: boolean
gender?: string | null
guardian_email?: string | null
guardian_name?: string | null
guardian_phone?: string | null
house_number?: string | null
iban?: string | null
id?: string
is_archived?: boolean
is_founding_member?: boolean
is_honorary?: boolean
is_probationary?: boolean
is_retiree?: boolean
is_transferred?: boolean
is_youth?: boolean
last_name: string
member_number?: string | null
mobile?: string | null
notes?: string | null
online_access_blocked?: boolean
online_access_key?: string | null
phone?: string | null
phone2?: string | null
postal_code?: string | null
salutation?: string | null
sepa_bank_name?: string | null
sepa_mandate_date?: string | null
sepa_mandate_id?: string | null
sepa_mandate_reference?: string | null
sepa_mandate_sequence?: string | null
sepa_mandate_status?:
| Database["public"]["Enums"]["sepa_mandate_status"]
| null
status?: Database["public"]["Enums"]["membership_status"]
street?: string | null
street2?: string | null
title?: string | null
updated_at?: string
updated_by?: string | null
user_id?: string | null
}
Update: {
account_holder?: string | null
account_id?: string
additional_fees?: number | null
address_invalid?: boolean
bic?: string | null
birth_country?: string | null
birthplace?: string | null
city?: string | null
country?: string | null
created_at?: string
created_by?: string | null
custom_data?: Json
data_reconciliation_needed?: boolean
date_of_birth?: string | null
dues_category_id?: string | null
dues_paid?: boolean
dues_year?: number | null
email?: string | null
email_confirmed?: boolean
entry_date?: string
exemption_amount?: number | null
exemption_reason?: string | null
exemption_type?: string | null
exit_date?: string | null
exit_reason?: string | null
fax?: string | null
first_name?: string
gdpr_birthday_info?: boolean
gdpr_consent?: boolean
gdpr_consent_date?: string | null
gdpr_data_source?: string | null
gdpr_internet?: boolean
gdpr_newsletter?: boolean
gdpr_print?: boolean
gender?: string | null
guardian_email?: string | null
guardian_name?: string | null
guardian_phone?: string | null
house_number?: string | null
iban?: string | null
id?: string
is_archived?: boolean
is_founding_member?: boolean
is_honorary?: boolean
is_probationary?: boolean
is_retiree?: boolean
is_transferred?: boolean
is_youth?: boolean
last_name?: string
member_number?: string | null
mobile?: string | null
notes?: string | null
online_access_blocked?: boolean
online_access_key?: string | null
phone?: string | null
phone2?: string | null
postal_code?: string | null
salutation?: string | null
sepa_bank_name?: string | null
sepa_mandate_date?: string | null
sepa_mandate_id?: string | null
sepa_mandate_reference?: string | null
sepa_mandate_sequence?: string | null
sepa_mandate_status?:
| Database["public"]["Enums"]["sepa_mandate_status"]
| null
status?: Database["public"]["Enums"]["membership_status"]
street?: string | null
street2?: string | null
title?: string | null
updated_at?: string
updated_by?: string | null
user_id?: string | null
}
Relationships: [
{
@@ -2412,6 +2858,64 @@ export type Database = {
},
]
}
newsletter_subscriptions: {
Row: {
account_id: string
confirmation_token: string | null
confirmed_at: string | null
email: string
id: string
is_active: boolean
name: string | null
subscribed_at: string
unsubscribed_at: string | null
}
Insert: {
account_id: string
confirmation_token?: string | null
confirmed_at?: string | null
email: string
id?: string
is_active?: boolean
name?: string | null
subscribed_at?: string
unsubscribed_at?: string | null
}
Update: {
account_id?: string
confirmation_token?: string | null
confirmed_at?: string | null
email?: string
id?: string
is_active?: boolean
name?: string | null
subscribed_at?: string
unsubscribed_at?: string | null
}
Relationships: [
{
foreignKeyName: "newsletter_subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "newsletter_subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "newsletter_subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
newsletter_templates: {
Row: {
account_id: string
@@ -3015,6 +3519,259 @@ export type Database = {
},
]
}
sepa_mandates: {
Row: {
account_holder: string
account_id: string
bic: string | null
created_at: string
has_error: boolean
iban: string
id: string
is_primary: boolean
last_used_at: string | null
mandate_date: string
mandate_reference: string
member_id: string
notes: string | null
sequence: string
status: Database["public"]["Enums"]["sepa_mandate_status"]
updated_at: string
}
Insert: {
account_holder: string
account_id: string
bic?: string | null
created_at?: string
has_error?: boolean
iban: string
id?: string
is_primary?: boolean
last_used_at?: string | null
mandate_date: string
mandate_reference: string
member_id: string
notes?: string | null
sequence?: string
status?: Database["public"]["Enums"]["sepa_mandate_status"]
updated_at?: string
}
Update: {
account_holder?: string
account_id?: string
bic?: string | null
created_at?: string
has_error?: boolean
iban?: string
id?: string
is_primary?: boolean
last_used_at?: string | null
mandate_date?: string
mandate_reference?: string
member_id?: string
notes?: string | null
sequence?: string
status?: Database["public"]["Enums"]["sepa_mandate_status"]
updated_at?: string
}
Relationships: [
{
foreignKeyName: "sepa_mandates_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "sepa_mandates_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "sepa_mandates_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "sepa_mandates_member_id_fkey"
columns: ["member_id"]
isOneToOne: false
referencedRelation: "members"
referencedColumns: ["id"]
},
]
}
site_pages: {
Row: {
account_id: string
created_at: string
created_by: string | null
id: string
is_homepage: boolean
is_members_only: boolean
is_published: boolean
meta_description: string | null
meta_image: string | null
published_at: string | null
puck_data: Json
slug: string
sort_order: number
title: string
updated_at: string
updated_by: string | null
}
Insert: {
account_id: string
created_at?: string
created_by?: string | null
id?: string
is_homepage?: boolean
is_members_only?: boolean
is_published?: boolean
meta_description?: string | null
meta_image?: string | null
published_at?: string | null
puck_data?: Json
slug: string
sort_order?: number
title: string
updated_at?: string
updated_by?: string | null
}
Update: {
account_id?: string
created_at?: string
created_by?: string | null
id?: string
is_homepage?: boolean
is_members_only?: boolean
is_published?: boolean
meta_description?: string | null
meta_image?: string | null
published_at?: string | null
puck_data?: Json
slug?: string
sort_order?: number
title?: string
updated_at?: string
updated_by?: string | null
}
Relationships: [
{
foreignKeyName: "site_pages_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "site_pages_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "site_pages_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
site_settings: {
Row: {
account_id: string
contact_address: string | null
contact_email: string | null
contact_phone: string | null
created_at: string
custom_css: string | null
custom_domain: string | null
datenschutz: string | null
font_family: string | null
footer_text: string | null
impressum: string | null
is_public: boolean
navigation: Json
primary_color: string | null
secondary_color: string | null
site_logo: string | null
site_name: string | null
social_links: Json | null
updated_at: string
}
Insert: {
account_id: string
contact_address?: string | null
contact_email?: string | null
contact_phone?: string | null
created_at?: string
custom_css?: string | null
custom_domain?: string | null
datenschutz?: string | null
font_family?: string | null
footer_text?: string | null
impressum?: string | null
is_public?: boolean
navigation?: Json
primary_color?: string | null
secondary_color?: string | null
site_logo?: string | null
site_name?: string | null
social_links?: Json | null
updated_at?: string
}
Update: {
account_id?: string
contact_address?: string | null
contact_email?: string | null
contact_phone?: string | null
created_at?: string
custom_css?: string | null
custom_domain?: string | null
datenschutz?: string | null
font_family?: string | null
footer_text?: string | null
impressum?: string | null
is_public?: boolean
navigation?: Json
primary_color?: string | null
secondary_color?: string | null
site_logo?: string | null
site_name?: string | null
social_links?: Json | null
updated_at?: string
}
Relationships: [
{
foreignKeyName: "site_settings_account_id_fkey"
columns: ["account_id"]
isOneToOne: true
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "site_settings_account_id_fkey"
columns: ["account_id"]
isOneToOne: true
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "site_settings_account_id_fkey"
columns: ["account_id"]
isOneToOne: true
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
subscription_items: {
Row: {
created_at: string
@@ -3194,6 +3951,22 @@ export type Database = {
Args: { target_team_account_id: string; target_user_id: string }
Returns: boolean
}
check_duplicate_member: {
Args: {
p_account_id: string
p_date_of_birth?: string
p_first_name: string
p_last_name: string
}
Returns: {
date_of_birth: string
first_name: string
id: string
last_name: string
member_number: string
status: Database["public"]["Enums"]["membership_status"]
}[]
}
create_invitation: {
Args: { account_id: string; email: string; role: string }
Returns: {
@@ -3327,6 +4100,10 @@ export type Database = {
Args: { account_id: string; user_id: string }
Returns: boolean
}
link_member_to_user: {
Args: { p_invite_token: string; p_user_id: string }
Returns: string
}
module_query: {
Args: {
p_filters?: Json