Files
metabuilder/frontends/qt6/AdminView.qml
johndoe6345789 c4f72ded99 feat(qt6): MD3 rework all views — Dashboard, Profile, Admin, SuperGod, Comments, Settings
- Fix CCard content nesting (no anchors.fill inside CCard)
- chipColor/badgeColor string→Theme color fixes
- anchors-in-layout warnings resolved
- Tonal surfaces, proper MD3 spacing
- CButton replaces hand-rolled Rectangle buttons
- All 6 views preserved with full functionality

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:26:42 +00:00

956 lines
46 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
function loadEntityData() {
if (!useLiveData) return;
dbal.list(selectedEntity, { take: pageSize, skip: currentPage * pageSize }, function(result, error) {
if (error || !result) return;
var items = result.items || [];
var fields = entityFields[selectedEntity] || [];
var liveRecords = [];
for (var i = 0; i < items.length; i++) {
var rec = {};
for (var f = 0; f < fields.length; f++) {
rec[fields[f]] = items[i][fields[f]] || "";
}
liveRecords.push(rec);
}
var updated = Object.assign({}, records);
updated[selectedEntity] = liveRecords;
records = updated;
});
}
Component.onCompleted: {
if (useLiveData) loadEntityData();
}
onUseLiveDataChanged: {
if (useLiveData) loadEntityData();
}
// ── State ──────────────────────────────────────────────────────
property string selectedEntity: "User"
property string searchText: ""
property string activeFilter: "All"
property int currentPage: 0
property int pageSize: 5
property int selectedRow: -1
property var selectedRows: ({})
property bool selectAll: false
property bool createDialogOpen: false
property bool editDialogOpen: false
property bool deleteDialogOpen: false
property int editingIndex: -1
property var editingRecord: ({})
// ── Entity definitions ─────────────────────────────────────────
property var entities: [
"User", "Session", "Workflow", "Package", "UiPage",
"Credential", "Forum", "Notification", "AuditLog", "Media"
]
property var entityIcons: ({
"User": "\u{1F464}", "Session": "\u{1F513}", "Workflow": "\u{2699}",
"Package": "\u{1F4E6}", "UiPage": "\u{1F4C4}", "Credential": "\u{1F511}",
"Forum": "\u{1F4AC}", "Notification": "\u{1F514}", "AuditLog": "\u{1F4CB}",
"Media": "\u{1F3AC}"
})
// Column schemas per entity
property var entityColumns: ({
"User": ["ID", "Username", "Email", "Role", "Status", "Created"],
"Session": ["ID", "User", "IP Address", "Status", "Started", "Expires"],
"Workflow": ["ID", "Name", "Trigger", "Nodes", "Status", "Last Run"],
"Package": ["ID", "Name", "Version", "Author", "Status", "Installed"],
"UiPage": ["ID", "Title", "Route", "Layout", "Status", "Modified"],
"Credential": ["ID", "Label", "Type", "Scope", "Status", "Created"],
"Forum": ["ID", "Title", "Author", "Replies", "Status", "Created"],
"Notification": ["ID", "Type", "Recipient", "Message", "Status", "Sent"],
"AuditLog": ["ID", "Action", "User", "Resource", "Status", "Timestamp"],
"Media": ["ID", "Filename", "Type", "Size", "Status", "Uploaded"]
})
// Field keys matching columns (for record objects)
property var entityFields: ({
"User": ["id", "username", "email", "role", "status", "created"],
"Session": ["id", "user", "ip", "status", "started", "expires"],
"Workflow": ["id", "name", "trigger", "nodes", "status", "lastRun"],
"Package": ["id", "name", "version", "author", "status", "installed"],
"UiPage": ["id", "title", "route", "layout", "status", "modified"],
"Credential": ["id", "label", "type", "scope", "status", "created"],
"Forum": ["id", "title", "author", "replies", "status", "created"],
"Notification": ["id", "type", "recipient", "message", "status", "sent"],
"AuditLog": ["id", "action", "user", "resource", "status", "timestamp"],
"Media": ["id", "filename", "type", "size", "status", "uploaded"]
})
// ── Mock data store ────────────────────────────────────────────
property var records: ({
"User": [
{ id: "USR-001", username: "admin", email: "admin@metabuilder.io", role: "god", status: "Active", created: "2025-11-02" },
{ id: "USR-002", username: "jdoe", email: "jdoe@example.com", role: "admin", status: "Active", created: "2025-12-10" },
{ id: "USR-003", username: "alice", email: "alice@devteam.org", role: "editor", status: "Active", created: "2026-01-05" },
{ id: "USR-004", username: "bob_dev", email: "bob@contractor.io", role: "user", status: "Inactive", created: "2026-01-18" },
{ id: "USR-005", username: "carol", email: "carol@metabuilder.io", role: "admin", status: "Active", created: "2026-02-01" },
{ id: "USR-006", username: "dave", email: "dave@external.com", role: "user", status: "Active", created: "2026-02-14" },
{ id: "USR-007", username: "eve_sec", email: "eve@security.io", role: "auditor", status: "Active", created: "2026-03-01" },
{ id: "USR-008", username: "frank", email: "frank@legacy.net", role: "user", status: "Inactive", created: "2025-09-20" }
],
"Session": [
{ id: "SES-001", user: "admin", ip: "192.168.1.10", status: "Active", started: "2026-03-18 08:00", expires: "2026-03-18 20:00" },
{ id: "SES-002", user: "jdoe", ip: "10.0.0.42", status: "Active", started: "2026-03-18 09:15", expires: "2026-03-18 21:15" },
{ id: "SES-003", user: "alice", ip: "172.16.0.5", status: "Active", started: "2026-03-18 10:30", expires: "2026-03-18 22:30" },
{ id: "SES-004", user: "bob_dev", ip: "192.168.1.88", status: "Inactive", started: "2026-03-17 14:00", expires: "2026-03-17 23:59" },
{ id: "SES-005", user: "carol", ip: "10.0.0.101", status: "Active", started: "2026-03-18 07:45", expires: "2026-03-18 19:45" },
{ id: "SES-006", user: "dave", ip: "192.168.2.33", status: "Inactive", started: "2026-03-16 11:00", expires: "2026-03-16 23:00" }
],
"Workflow": [
{ id: "WF-001", name: "on_user_created", trigger: "User.created", nodes: "15", status: "Active", lastRun: "2026-03-18 09:01" },
{ id: "WF-002", name: "on_snippet_saved", trigger: "Snippet.created", nodes: "8", status: "Active", lastRun: "2026-03-18 08:45" },
{ id: "WF-003", name: "nightly_cleanup", trigger: "cron:0 2 * * *", nodes: "12", status: "Active", lastRun: "2026-03-18 02:00" },
{ id: "WF-004", name: "on_login_failed", trigger: "Auth.failed", nodes: "6", status: "Active", lastRun: "2026-03-17 23:12" },
{ id: "WF-005", name: "weekly_report", trigger: "cron:0 9 * * MON", nodes: "20", status: "Inactive", lastRun: "2026-03-11 09:00" },
{ id: "WF-006", name: "media_transcode", trigger: "Media.uploaded", nodes: "10", status: "Active", lastRun: "2026-03-18 07:30" }
],
"Package": [
{ id: "PKG-001", name: "forum", version: "2.1.0", author: "core-team", status: "Active", installed: "2025-12-01" },
{ id: "PKG-002", name: "guestbook", version: "1.3.2", author: "core-team", status: "Active", installed: "2025-12-01" },
{ id: "PKG-003", name: "notifications", version: "1.8.0", author: "core-team", status: "Active", installed: "2026-01-10" },
{ id: "PKG-004", name: "media-gallery", version: "3.0.1", author: "plugins", status: "Active", installed: "2026-01-15" },
{ id: "PKG-005", name: "irc-bridge", version: "0.9.0", author: "community", status: "Inactive", installed: "2026-02-05" },
{ id: "PKG-006", name: "analytics", version: "1.2.0", author: "core-team", status: "Active", installed: "2026-02-20" },
{ id: "PKG-007", name: "streaming", version: "1.0.0", author: "plugins", status: "Active", installed: "2026-03-01" }
],
"UiPage": [
{ id: "PG-001", title: "Dashboard", route: "/", layout: "full", status: "Active", modified: "2026-03-15" },
{ id: "PG-002", title: "User Profile", route: "/profile", layout: "sidebar", status: "Active", modified: "2026-03-10" },
{ id: "PG-003", title: "Settings", route: "/settings", layout: "full", status: "Active", modified: "2026-03-12" },
{ id: "PG-004", title: "Admin Panel", route: "/admin", layout: "sidebar", status: "Active", modified: "2026-03-18" },
{ id: "PG-005", title: "Workflow Editor", route: "/workflows", layout: "canvas", status: "Active", modified: "2026-03-14" },
{ id: "PG-006", title: "Legacy Import", route: "/import", layout: "full", status: "Inactive", modified: "2026-01-20" }
],
"Credential": [
{ id: "CRD-001", label: "SMTP Production", type: "smtp", scope: "global", status: "Active", created: "2025-12-01" },
{ id: "CRD-002", label: "AWS S3 Bucket", type: "aws-s3", scope: "media", status: "Active", created: "2026-01-10" },
{ id: "CRD-003", label: "GitHub Deploy Key", type: "ssh-key", scope: "ci-cd", status: "Active", created: "2026-01-20" },
{ id: "CRD-004", label: "Slack Webhook", type: "webhook", scope: "notifications", status: "Active", created: "2026-02-05" },
{ id: "CRD-005", label: "DB Staging", type: "database", scope: "staging", status: "Inactive", created: "2025-11-15" }
],
"Forum": [
{ id: "FRM-001", title: "Welcome to MetaBuilder", author: "admin", replies: "24", status: "Active", created: "2025-12-05" },
{ id: "FRM-002", title: "Bug: Workflow not firing", author: "jdoe", replies: "8", status: "Active", created: "2026-02-10" },
{ id: "FRM-003", title: "Feature: Dark mode toggle", author: "alice", replies: "15", status: "Active", created: "2026-02-18" },
{ id: "FRM-004", title: "How to create custom nodes?", author: "dave", replies: "6", status: "Active", created: "2026-03-02" },
{ id: "FRM-005", title: "Migration guide v1 to v2", author: "carol", replies: "31", status: "Active", created: "2026-01-25" },
{ id: "FRM-006", title: "Deprecated: Old API docs", author: "admin", replies: "2", status: "Inactive", created: "2025-10-10" }
],
"Notification": [
{ id: "NTF-001", type: "system", recipient: "all", message: "Maintenance window 03/20", status: "Active", sent: "2026-03-18 06:00" },
{ id: "NTF-002", type: "alert", recipient: "admin", message: "High CPU on dbal-prod", status: "Active", sent: "2026-03-18 07:30" },
{ id: "NTF-003", type: "info", recipient: "jdoe", message: "Your export is ready", status: "Active", sent: "2026-03-18 09:00" },
{ id: "NTF-004", type: "warning", recipient: "eve_sec", message: "3 failed login attempts", status: "Active", sent: "2026-03-18 08:15" },
{ id: "NTF-005", type: "system", recipient: "all", message: "v2.1.0 deployed", status: "Inactive", sent: "2026-03-15 12:00" }
],
"AuditLog": [
{ id: "AUD-001", action: "user.login", user: "admin", resource: "auth/session", status: "Active", timestamp: "2026-03-18 08:00" },
{ id: "AUD-002", action: "record.create", user: "jdoe", resource: "forum/post", status: "Active", timestamp: "2026-03-18 08:30" },
{ id: "AUD-003", action: "record.update", user: "alice", resource: "workflow/WF-001", status: "Active", timestamp: "2026-03-18 09:01" },
{ id: "AUD-004", action: "user.logout", user: "bob_dev", resource: "auth/session", status: "Active", timestamp: "2026-03-17 18:00" },
{ id: "AUD-005", action: "record.delete", user: "admin", resource: "media/MED-003", status: "Active", timestamp: "2026-03-18 07:45" },
{ id: "AUD-006", action: "config.change", user: "carol", resource: "settings/smtp", status: "Active", timestamp: "2026-03-17 16:30" },
{ id: "AUD-007", action: "auth.failed", user: "unknown", resource: "auth/login", status: "Active", timestamp: "2026-03-18 08:12" }
],
"Media": [
{ id: "MED-001", filename: "logo-dark.svg", type: "image/svg", size: "12 KB", status: "Active", uploaded: "2026-01-05" },
{ id: "MED-002", filename: "hero-banner.png", type: "image/png", size: "2.4 MB", status: "Active", uploaded: "2026-02-10" },
{ id: "MED-003", filename: "intro-video.mp4", type: "video/mp4", size: "48 MB", status: "Active", uploaded: "2026-02-20" },
{ id: "MED-004", filename: "user-guide.pdf", type: "application/pdf", size: "1.1 MB", status: "Active", uploaded: "2026-03-01" },
{ id: "MED-005", filename: "old-theme.css", type: "text/css", size: "85 KB", status: "Inactive", uploaded: "2025-09-15" },
{ id: "MED-006", filename: "avatar-defaults.zip", type: "application/zip", size: "5.6 MB", status: "Active", uploaded: "2026-03-10" }
]
})
// ── Computed helpers ────────────────────────────────────────────
function getFilteredRecords() {
var data = records[selectedEntity] || [];
var result = [];
for (var i = 0; i < data.length; i++) {
var rec = data[i];
// Filter by status
if (activeFilter === "Active" && rec.status !== "Active") continue;
if (activeFilter === "Inactive" && rec.status !== "Inactive") continue;
// Filter by search text
if (searchText.length > 0) {
var fields = entityFields[selectedEntity];
var match = false;
for (var f = 0; f < fields.length; f++) {
if (String(rec[fields[f]]).toLowerCase().indexOf(searchText.toLowerCase()) >= 0) {
match = true;
break;
}
}
if (!match) continue;
}
result.push(rec);
}
return result;
}
function getPagedRecords() {
var filtered = getFilteredRecords();
var start = currentPage * pageSize;
return filtered.slice(start, start + pageSize);
}
function totalFiltered() {
return getFilteredRecords().length;
}
function totalPages() {
return Math.max(1, Math.ceil(totalFiltered() / pageSize));
}
function statCount(entity) {
return (records[entity] || []).length;
}
function generateId() {
var prefixes = { "User": "USR", "Session": "SES", "Workflow": "WF", "Package": "PKG",
"UiPage": "PG", "Credential": "CRD", "Forum": "FRM", "Notification": "NTF",
"AuditLog": "AUD", "Media": "MED" };
var prefix = prefixes[selectedEntity] || "REC";
var num = (records[selectedEntity] || []).length + 1;
return prefix + "-" + String(num).padStart(3, '0');
}
function deleteRecord(idx) {
var data = records[selectedEntity].slice();
var actualRec = getPagedRecords()[idx];
for (var i = 0; i < data.length; i++) {
if (data[i].id === actualRec.id) {
data.splice(i, 1);
break;
}
}
var updated = Object.assign({}, records);
updated[selectedEntity] = data;
records = updated;
selectedRow = -1;
if (currentPage >= totalPages()) currentPage = Math.max(0, totalPages() - 1);
}
function deleteSelectedRows() {
var data = records[selectedEntity].slice();
var pagedRecs = getPagedRecords();
var idsToDelete = {};
for (var key in selectedRows) {
if (selectedRows[key]) {
var rec = pagedRecs[parseInt(key)];
if (rec) idsToDelete[rec.id] = true;
}
}
var newData = [];
for (var i = 0; i < data.length; i++) {
if (!idsToDelete[data[i].id]) newData.push(data[i]);
}
var updated = Object.assign({}, records);
updated[selectedEntity] = newData;
records = updated;
selectedRows = {};
selectAll = false;
selectedRow = -1;
if (currentPage >= totalPages()) currentPage = Math.max(0, totalPages() - 1);
}
function addRecord(rec) {
var data = records[selectedEntity].slice();
data.push(rec);
var updated = Object.assign({}, records);
updated[selectedEntity] = data;
records = updated;
}
function updateRecord(rec) {
var data = records[selectedEntity].slice();
var pagedRecs = getPagedRecords();
var targetId = pagedRecs[editingIndex] ? pagedRecs[editingIndex].id : "";
for (var i = 0; i < data.length; i++) {
if (data[i].id === targetId) {
data[i] = rec;
break;
}
}
var updated = Object.assign({}, records);
updated[selectedEntity] = data;
records = updated;
}
function hasSelectedRows() {
for (var key in selectedRows) {
if (selectedRows[key]) return true;
}
return false;
}
// Reset pagination/selection on entity change
onSelectedEntityChanged: {
currentPage = 0;
selectedRow = -1;
selectedRows = {};
selectAll = false;
searchText = "";
activeFilter = "All";
if (useLiveData) loadEntityData();
}
// ── Layout ─────────────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
spacing: 0
// ── Stats bar ──────────────────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 88
color: Theme.surface
radius: 0
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
Repeater {
model: [
{ label: "Total Users", key: "User", accent: "#4CAF50" },
{ label: "Active Sessions", key: "Session", accent: "#2196F3" },
{ label: "Workflows", key: "Workflow", accent: "#FF9800" },
{ label: "Audit Events", key: "AuditLog", accent: "#9C27B0" }
]
delegate: CCard {
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
Layout.fillWidth: true
spacing: 8
Rectangle {
width: 4
Layout.fillHeight: true
color: modelData.accent
radius: 2
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CText { variant: "caption"; text: modelData.label; color: Theme.textSecondary }
CText { variant: "h3"; text: String(statCount(modelData.key)) }
}
}
}
}
}
}
// ── Main content row ───────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
// ── Entity sidebar ─────────────────────────────────────
Rectangle {
Layout.preferredWidth: 220
Layout.fillHeight: true
color: Theme.surface
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "h4"; text: "Entities" }
CText { variant: "caption"; text: "God Panel Level 3"; color: Theme.textSecondary }
CDivider { Layout.fillWidth: true; Layout.topMargin: 8; Layout.bottomMargin: 4 }
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: entities
spacing: 2
clip: true
delegate: CListItem {
width: parent ? parent.width : 200
title: modelData
subtitle: statCount(modelData) + " records"
leadingIcon: entityIcons[modelData] || ""
selected: selectedEntity === modelData
onClicked: selectedEntity = modelData
}
}
}
}
// ── Main data area ─────────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.background
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// ── Title row ──────────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h3"; text: (entityIcons[selectedEntity] || "") + " " + selectedEntity + " Management" }
CStatusBadge {
status: useLiveData ? "success" : "warning"
text: useLiveData ? "Live" : "Mock"
}
Item { Layout.fillWidth: true }
CButton {
text: "Create Record"
variant: "primary"
size: "sm"
onClicked: {
editingRecord = {};
createDialogOpen = true;
}
}
}
// ── Search + filters ───────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 8
CTextField {
Layout.preferredWidth: 280
label: "Search"
placeholderText: "Filter " + selectedEntity.toLowerCase() + " records..."
text: searchText
onTextChanged: { searchText = text; currentPage = 0; }
}
Item { Layout.preferredWidth: 12 }
Repeater {
model: ["All", "Active", "Inactive"]
delegate: CChip {
text: modelData
checked: activeFilter === modelData
chipColor: activeFilter === modelData ? Theme.primary : Theme.surface
onClicked: { activeFilter = modelData; currentPage = 0; }
}
}
Item { Layout.fillWidth: true }
CButton {
text: "Delete Selected"
variant: "danger"
size: "sm"
enabled: hasSelectedRows()
onClicked: deleteSelectedRows()
}
}
// ── Data table card ────────────────────────────
CCard {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
Layout.fillWidth: true
spacing: 0
// ── Column headers ────────────────────
Rectangle {
Layout.fillWidth: true
height: 44
color: Theme.surfaceVariant
radius: 0
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 0
// Select All checkbox
CheckBox {
Layout.preferredWidth: 36
checked: selectAll
onCheckedChanged: {
selectAll = checked;
var paged = getPagedRecords();
var newSel = {};
for (var i = 0; i < paged.length; i++) {
newSel[i] = checked;
}
selectedRows = newSel;
}
}
Repeater {
model: entityColumns[selectedEntity] || []
delegate: CText {
Layout.fillWidth: index > 0
Layout.preferredWidth: index === 0 ? 80 : -1
variant: "subtitle2"
text: modelData
}
}
CText {
Layout.preferredWidth: 110
variant: "subtitle2"
text: "Actions"
horizontalAlignment: Text.AlignRight
}
}
}
CDivider { Layout.fillWidth: true }
// ── Data rows ─────────────────────────
ListView {
id: tableView
Layout.fillWidth: true
Layout.fillHeight: true
model: getPagedRecords()
clip: true
spacing: 0
delegate: Rectangle {
id: rowDelegate
width: tableView.width
height: 48
property var rowData: modelData
property int rowIndex: index
color: {
if (selectedRow === rowIndex) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
if (selectedRows[rowIndex]) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
return rowIndex % 2 === 0 ? "transparent" : Theme.surfaceVariant;
}
radius: 0
MouseArea {
anchors.fill: parent
onClicked: selectedRow = rowDelegate.rowIndex
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 0
CheckBox {
Layout.preferredWidth: 36
checked: selectedRows[rowDelegate.rowIndex] || false
onCheckedChanged: {
var newSel = Object.assign({}, selectedRows);
newSel[rowDelegate.rowIndex] = checked;
selectedRows = newSel;
}
}
Repeater {
model: entityFields[selectedEntity] || []
delegate: Item {
Layout.fillWidth: index > 0
Layout.preferredWidth: index === 0 ? 80 : -1
implicitHeight: 48
CText {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
variant: "body2"
text: {
var key = modelData;
var rec = rowDelegate.rowData;
return rec ? (String(rec[key] || "")) : "";
}
elide: Text.ElideRight
}
}
}
FlexRow {
Layout.preferredWidth: 110
Layout.alignment: Qt.AlignRight
spacing: 4
CButton {
text: "Edit"
variant: "ghost"
size: "sm"
onClicked: {
editingIndex = rowDelegate.rowIndex;
editingRecord = Object.assign({}, rowDelegate.rowData);
editDialogOpen = true;
}
}
CButton {
text: "Del"
variant: "danger"
size: "sm"
onClicked: {
editingIndex = rowDelegate.rowIndex;
deleteDialogOpen = true;
}
}
}
}
}
}
// Empty state
Item {
Layout.fillWidth: true
Layout.fillHeight: totalFiltered() === 0
visible: totalFiltered() === 0
Layout.preferredHeight: visible ? 120 : 0
ColumnLayout {
anchors.centerIn: parent
spacing: 8
CText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
variant: "h4"
text: "No records found"
color: Theme.textSecondary
}
CText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
variant: "caption"
text: "Try adjusting your search or filter criteria."
color: Theme.textMuted
}
}
}
CDivider { Layout.fillWidth: true }
// ── Pagination footer ─────────────────
Rectangle {
Layout.fillWidth: true
height: 48
color: Theme.surfaceVariant
radius: 0
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 8
CText {
variant: "caption"
text: {
var total = totalFiltered();
if (total === 0) return "0 records";
var start = currentPage * pageSize + 1;
var end = Math.min(start + pageSize - 1, total);
return start + "-" + end + " of " + total + " records";
}
color: Theme.textSecondary
}
Item { Layout.fillWidth: true }
CButton {
text: "Previous"
variant: "ghost"
size: "sm"
enabled: currentPage > 0
onClicked: currentPage--
}
CText {
variant: "caption"
text: "Page " + (currentPage + 1) + " of " + totalPages()
color: Theme.textSecondary
}
CButton {
text: "Next"
variant: "ghost"
size: "sm"
enabled: currentPage < totalPages() - 1
onClicked: currentPage++
}
}
}
}
}
}
}
}
}
// ── Form data for CRUD dialogs ───────────────────────────────
property var createFormData: ({})
property var editFormData: ({})
function setCreateField(key, val) {
var d = Object.assign({}, createFormData);
d[key] = val;
createFormData = d;
}
function setEditField(key, val) {
var d = Object.assign({}, editFormData);
d[key] = val;
editFormData = d;
}
onCreateDialogOpenChanged: {
if (createDialogOpen) createFormData = {};
}
onEditDialogOpenChanged: {
if (editDialogOpen) {
var d = {};
var fields = entityFields[selectedEntity] || [];
for (var i = 0; i < fields.length; i++) {
d[fields[i]] = editingRecord[fields[i]] || "";
}
editFormData = d;
}
}
// ── CRUD Dialogs ───────────────────────────────────────────────
// Create Record Dialog
CDialog {
id: createDialog
visible: createDialogOpen
title: "Create " + selectedEntity
ColumnLayout {
width: 400
spacing: 12
Repeater {
model: {
var fields = entityFields[selectedEntity] || [];
var cols = entityColumns[selectedEntity] || [];
var result = [];
for (var i = 1; i < fields.length; i++) {
result.push({ field: fields[i], label: cols[i] || fields[i] });
}
return result;
}
delegate: CTextField {
Layout.fillWidth: true
label: modelData.label
placeholderText: "Enter " + modelData.label.toLowerCase() + "..."
onTextChanged: setCreateField(modelData.field, text)
}
}
FlexRow {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
size: "sm"
onClicked: createDialogOpen = false
}
CButton {
text: "Create"
variant: "primary"
size: "sm"
onClicked: {
var fields = entityFields[selectedEntity];
var newRec = { id: generateId() };
for (var f = 1; f < fields.length; f++) {
newRec[fields[f]] = createFormData[fields[f]] || "";
}
if (!newRec.status) newRec.status = "Active";
if (useLiveData) {
dbal.create(selectedEntity, newRec, function(result, error) {
if (!error) {
loadEntityData();
} else {
addRecord(newRec);
}
});
} else {
addRecord(newRec);
}
createDialogOpen = false;
}
}
}
}
}
// Edit Record Dialog
CDialog {
id: editDialog
visible: editDialogOpen
title: "Edit " + selectedEntity + " - " + (editingRecord.id || "")
ColumnLayout {
width: 400
spacing: 12
CText { variant: "caption"; text: "ID: " + (editingRecord.id || ""); color: Theme.textSecondary }
Repeater {
model: {
var fields = entityFields[selectedEntity] || [];
var cols = entityColumns[selectedEntity] || [];
var result = [];
for (var i = 1; i < fields.length; i++) {
result.push({ field: fields[i], label: cols[i] || fields[i], value: editingRecord[fields[i]] || "" });
}
return result;
}
delegate: CTextField {
Layout.fillWidth: true
label: modelData.label
text: modelData.value
placeholderText: "Enter " + modelData.label.toLowerCase() + "..."
onTextChanged: setEditField(modelData.field, text)
}
}
FlexRow {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
size: "sm"
onClicked: editDialogOpen = false
}
CButton {
text: "Save"
variant: "primary"
size: "sm"
onClicked: {
var fields = entityFields[selectedEntity];
var updatedRec = { id: editingRecord.id };
for (var f = 1; f < fields.length; f++) {
updatedRec[fields[f]] = editFormData[fields[f]] || editingRecord[fields[f]] || "";
}
if (useLiveData) {
dbal.update(selectedEntity, editingRecord.id, updatedRec, function(result, error) {
if (!error) {
loadEntityData();
} else {
updateRecord(updatedRec);
}
});
} else {
updateRecord(updatedRec);
}
editDialogOpen = false;
}
}
}
}
}
// Delete Confirmation Dialog
CDialog {
id: deleteConfirmDialog
visible: deleteDialogOpen
title: "Delete " + selectedEntity
ColumnLayout {
width: 360
spacing: 16
CAlert {
Layout.fillWidth: true
severity: "warning"
text: "This action cannot be undone."
}
CText {
Layout.fillWidth: true
variant: "body1"
text: {
var paged = getPagedRecords();
var rec = paged[editingIndex];
if (!rec) return "Delete this record?";
return "Are you sure you want to delete record " + rec.id + "?";
}
}
FlexRow {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
size: "sm"
onClicked: deleteDialogOpen = false
}
CButton {
text: "Delete"
variant: "danger"
size: "sm"
onClicked: {
if (useLiveData) {
var paged = getPagedRecords();
var rec = paged[editingIndex];
if (rec) {
dbal.remove(selectedEntity, rec.id, function(result, error) {
if (!error) {
loadEntityData();
} else {
deleteRecord(editingIndex);
}
});
}
} else {
deleteRecord(editingIndex);
}
deleteDialogOpen = false;
}
}
}
}
}
}