/**
* Plugin Name: MR Business Grid
* Description: Light no-code business workspace with spreadsheet-like cells, scenes, formulas, fields, widgets, copy/paste from Sheets, and custom CSS/JavaScript.
* Version: 0.3.1
* Author: Music Road
* Text Domain: mr-business-builder
* Requires at least: 6.4
* Requires PHP: 8.0
*/
if (!defined('ABSPATH')) {
exit;
}
define('MRBB_VERSION', '0.3.1');
define('MRBB_FILE', __FILE__);
define('MRBB_DIR', plugin_dir_path(__FILE__));
define('MRBB_URL', plugin_dir_url(__FILE__));
final class MRBB_Business_Grid {
private const CAPABILITY = 'mrbb_manage_workspaces';
private const DB_VERSION = '0.3.1';
private const MENU_SLUG = 'mr-business-builder';
private const MAX_CONFIG_BYTES = 8_000_000;
public static function boot(): void {
register_activation_hook(MRBB_FILE, [__CLASS__, 'activate']);
add_action('plugins_loaded', [__CLASS__, 'maybe_upgrade']);
add_action('admin_menu', [__CLASS__, 'admin_menu']);
add_action('init', [__CLASS__, 'register_assets']);
add_action('admin_enqueue_scripts', [__CLASS__, 'admin_assets']);
add_action('wp_enqueue_scripts', [__CLASS__, 'front_assets'], 20);
add_action('wp_ajax_mrbb_save_workspace', [__CLASS__, 'ajax_save_workspace']);
add_action('wp_ajax_mrbb_create_workspace', [__CLASS__, 'ajax_create_workspace']);
add_action('wp_ajax_mrbb_delete_workspace', [__CLASS__, 'ajax_delete_workspace']);
add_shortcode('mr_business_grid', [__CLASS__, 'shortcode']);
add_shortcode('mr_business_app', [__CLASS__, 'shortcode']);
add_shortcode('mr_business_studio', [__CLASS__, 'shortcode']);
}
private static function apps_table(): string {
global $wpdb;
return $wpdb->prefix . 'mrbb_apps';
}
private static function records_table(): string {
global $wpdb;
return $wpdb->prefix . 'mrbb_records';
}
public static function activate(): void {
self::create_tables();
self::add_capability();
self::ensure_default_workspace();
update_option('mrbb_db_version', self::DB_VERSION, false);
}
public static function maybe_upgrade(): void {
if ((string) get_option('mrbb_db_version', '') !== self::DB_VERSION) {
self::create_tables();
self::add_capability();
update_option('mrbb_db_version', self::DB_VERSION, false);
}
self::ensure_default_workspace();
}
private static function create_tables(): void {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset = $wpdb->get_charset_collate();
$apps = self::apps_table();
$records = self::records_table();
dbDelta("CREATE TABLE {$apps} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
name varchar(190) NOT NULL,
slug varchar(190) NOT NULL,
description text NULL,
config longtext NOT NULL,
custom_css longtext NULL,
custom_js longtext NULL,
status varchar(20) NOT NULL DEFAULT 'draft',
created_by bigint(20) unsigned NOT NULL DEFAULT 0,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY slug (slug),
KEY status (status),
KEY created_by (created_by)
) {$charset};");
dbDelta("CREATE TABLE {$records} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
app_id bigint(20) unsigned NOT NULL,
data longtext NOT NULL,
created_by bigint(20) unsigned NOT NULL DEFAULT 0,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id),
KEY app_id (app_id),
KEY created_at (created_at)
) {$charset};");
}
private static function add_capability(): void {
$administrator = get_role('administrator');
if ($administrator && !$administrator->has_cap(self::CAPABILITY)) {
$administrator->add_cap(self::CAPABILITY);
}
}
private static function require_capability(): void {
if (!current_user_can(self::CAPABILITY) && !current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Δεν έχετε δικαίωμα επεξεργασίας.'], 403);
}
}
private static function unique_slug(string $name, int $exclude_id = 0): string {
global $wpdb;
$base = sanitize_title($name) ?: 'business-workspace';
$slug = $base;
$suffix = 2;
$table = self::apps_table();
while (true) {
if ($exclude_id > 0) {
$exists = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE slug = %s AND id != %d", $slug, $exclude_id));
} else {
$exists = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE slug = %s", $slug));
}
if ($exists === 0) {
return $slug;
}
$slug = $base . '-' . $suffix++;
}
}
private static function ensure_default_workspace(): int {
global $wpdb;
$table = self::apps_table();
$first = (int) $wpdb->get_var("SELECT id FROM {$table} ORDER BY id ASC LIMIT 1");
if ($first > 0) {
$row = self::get_workspace_row($first);
if ($row) {
$decoded = json_decode((string) $row['config'], true);
if (!is_array($decoded) || empty($decoded['scenes']) || (int) ($decoded['schemaVersion'] ?? 0) < 3) {
$workspace = self::default_workspace();
$workspace['legacyConfig'] = is_array($decoded) ? $decoded : null;
self::import_legacy_names($workspace, $first);
$wpdb->update(
$table,
[
'name' => 'Η επιχείρησή μου',
'description' => 'Πελάτες, πληρωμές, υπάλληλοι, πρόγραμμα και οικονομικά.',
'config' => wp_json_encode($workspace, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'updated_at' => current_time('mysql'),
],
['id' => $first],
['%s', '%s', '%s', '%s'],
['%d']
);
}
}
return $first;
}
$now = current_time('mysql');
$name = 'Η επιχείρησή μου';
$wpdb->insert(
$table,
[
'name' => $name,
'slug' => self::unique_slug($name),
'description' => 'Πελάτες, πληρωμές, υπάλληλοι, πρόγραμμα και οικονομικά.',
'config' => wp_json_encode(self::default_workspace(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'custom_css' => '',
'custom_js' => '',
'status' => 'published',
'created_by' => get_current_user_id(),
'created_at' => $now,
'updated_at' => $now,
],
['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s']
);
return (int) $wpdb->insert_id;
}
private static function import_legacy_names(array &$workspace, int $app_id): void {
global $wpdb;
$rows = $wpdb->get_results(
$wpdb->prepare('SELECT data FROM ' . self::records_table() . ' WHERE app_id = %d ORDER BY id ASC LIMIT 500', $app_id),
ARRAY_A
);
if (!is_array($rows) || !$rows) {
return;
}
$scene_index = self::scene_index($workspace, 'clients');
if ($scene_index < 0) {
return;
}
$row_number = 2;
foreach ($rows as $row) {
$data = json_decode((string) ($row['data'] ?? ''), true);
if (!is_array($data)) {
continue;
}
$name = trim((string) ($data['student_name'] ?? $data['client_name'] ?? $data['customer_name'] ?? $data['name'] ?? ''));
if ($name === '') {
continue;
}
$workspace['scenes'][$scene_index]['cells']['B' . $row_number] = self::cell($name, 'text');
foreach (['phone' => 'C', 'instrument' => 'D', 'monthly_fee' => 'E', 'payment' => 'F', 'payment_method' => 'H', 'month' => 'I', 'status' => 'J'] as $key => $column) {
if (isset($data[$key]) && $data[$key] !== '') {
$type = in_array($key, ['monthly_fee', 'payment'], true) ? 'currency' : 'text';
$workspace['scenes'][$scene_index]['cells'][$column . $row_number] = self::cell($data[$key], $type);
}
}
$row_number++;
if ($row_number > 80) {
break;
}
}
}
private static function get_workspace_row(int $id): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare('SELECT * FROM ' . self::apps_table() . ' WHERE id = %d', $id), ARRAY_A);
return is_array($row) ? $row : null;
}
private static function get_first_workspace(bool $published_only = false): ?array {
global $wpdb;
$table = self::apps_table();
$where = $published_only ? "WHERE status = 'published'" : '';
$row = $wpdb->get_row("SELECT * FROM {$table} {$where} ORDER BY updated_at DESC, id DESC LIMIT 1", ARRAY_A);
return is_array($row) ? $row : null;
}
private static function get_workspaces(): array {
global $wpdb;
$rows = $wpdb->get_results('SELECT id,name,slug,status,updated_at FROM ' . self::apps_table() . ' ORDER BY updated_at DESC, id DESC', ARRAY_A);
return is_array($rows) ? $rows : [];
}
private static function scene_index(array $workspace, string $scene_id): int {
foreach (($workspace['scenes'] ?? []) as $index => $scene) {
if ((string) ($scene['id'] ?? '') === $scene_id) {
return (int) $index;
}
}
return -1;
}
public static function register_assets(): void {
if (!wp_style_is('mrbb-business-grid', 'registered')) {
wp_register_style('mrbb-business-grid', MRBB_URL . 'mr-business-builder.css', [], self::asset_version('mr-business-builder.css'));
}
if (!wp_script_is('mrbb-business-grid', 'registered')) {
wp_register_script('mrbb-business-grid', MRBB_URL . 'mr-business-builder.js', [], self::asset_version('mr-business-builder.js'), true);
}
}
private static function enqueue_assets(): void {
self::register_assets();
wp_enqueue_style('mrbb-business-grid');
wp_enqueue_script('mrbb-business-grid');
}
public static function front_assets(): void {
global $post;
if (!is_singular() || !($post instanceof WP_Post)) {
return;
}
$content = (string) $post->post_content;
if (
has_shortcode($content, 'mr_business_grid') ||
has_shortcode($content, 'mr_business_app') ||
has_shortcode($content, 'mr_business_studio')
) {
self::enqueue_assets();
}
}
private static function asset_version(string $file): string {
$path = MRBB_DIR . $file;
return is_file($path) ? (string) filemtime($path) : MRBB_VERSION;
}
private static function missing_assets(): array {
$missing = [];
foreach (['mr-business-builder.js', 'mr-business-builder.css'] as $file) {
if (!is_file(MRBB_DIR . $file)) {
$missing[] = $file;
}
}
return $missing;
}
public static function admin_menu(): void {
add_menu_page(
'MR Business Grid',
'MR Business Grid',
self::CAPABILITY,
self::MENU_SLUG,
[__CLASS__, 'admin_page'],
'dashicons-screenoptions',
3
);
}
public static function admin_assets(string $hook): void {
if ($hook !== 'toplevel_page_' . self::MENU_SLUG) {
return;
}
self::enqueue_assets();
}
public static function admin_page(): void {
if (!current_user_can(self::CAPABILITY) && !current_user_can('manage_options')) {
wp_die('Δεν έχετε δικαίωμα πρόσβασης.');
}
$missing_assets = self::missing_assets();
if ($missing_assets) {
wp_die('Λείπουν τα αρχεία του plugin: ' . esc_html(implode(', ', $missing_assets)) . '. Τα ονόματα πρέπει να είναι ακριβώς mr-business-builder.php, mr-business-builder.js και mr-business-builder.css.');
}
$default_id = self::ensure_default_workspace();
$requested = isset($_GET['app_id']) ? absint($_GET['app_id']) : $default_id;
$row = self::get_workspace_row($requested) ?: self::get_workspace_row($default_id);
if (!$row) {
wp_die('Δεν βρέθηκε χώρος εργασίας.');
}
$workspace = json_decode((string) $row['config'], true);
if (!is_array($workspace) || empty($workspace['scenes'])) {
$workspace = self::default_workspace();
}
$boot = [
'context' => 'admin',
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('mrbb_workspace'),
'app' => [
'id' => (int) $row['id'],
'name' => (string) $row['name'],
'description' => (string) ($row['description'] ?? ''),
'status' => (string) $row['status'],
'customCss' => (string) ($row['custom_css'] ?? ''),
'customJs' => current_user_can('unfiltered_html') ? (string) ($row['custom_js'] ?? '') : '',
],
'workspace' => $workspace,
'workspaces' => self::get_workspaces(),
'permissions' => [
'canEdit' => true,
'canCode' => current_user_can('unfiltered_html'),
],
'strings' => self::strings(),
];
echo '
';
echo '
';
echo '';
echo '
';
}
public static function shortcode(array $atts = []): string {
$missing_assets = self::missing_assets();
if ($missing_assets) {
return 'Λείπουν αρχεία του MR Business Grid.' . esc_html(implode(', ', $missing_assets)) . '
';
}
self::enqueue_assets();
$atts = shortcode_atts([
'id' => 0,
'editable' => 'auto',
'height' => '760px',
], $atts, 'mr_business_grid');
$id = absint($atts['id']);
$row = $id > 0 ? self::get_workspace_row($id) : self::get_first_workspace(true);
if (!$row) {
$row = self::get_first_workspace(false);
}
if (!$row) {
return 'Δεν βρέθηκε εφαρμογή.
';
}
$workspace = json_decode((string) $row['config'], true);
if (!is_array($workspace) || empty($workspace['scenes'])) {
return 'Η εφαρμογή δεν έχει έγκυρη δομή.
';
}
$editable_mode = strtolower(trim((string) $atts['editable']));
$user_can_edit = current_user_can(self::CAPABILITY) || current_user_can('manage_options');
$can_edit = $user_can_edit && in_array($editable_mode, ['yes', 'true', '1', 'auto'], true);
$instance = 'mrbb-front-' . wp_generate_uuid4();
$boot = [
'context' => 'front',
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('mrbb_workspace'),
'app' => [
'id' => (int) $row['id'],
'name' => (string) $row['name'],
'description' => (string) ($row['description'] ?? ''),
'status' => (string) $row['status'],
'customCss' => (string) ($row['custom_css'] ?? ''),
'customJs' => (string) ($row['custom_js'] ?? ''),
],
'workspace' => $workspace,
'permissions' => [
'canEdit' => $can_edit,
'canCode' => false,
],
'strings' => self::strings(),
];
$height = preg_match('/^\d+(px|vh|rem|em|%)$/', (string) $atts['height']) ? (string) $atts['height'] : '760px';
$json = wp_json_encode($boot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json) || $json === '') {
return 'Αποτυχία δημιουργίας δεδομένων εκκίνησης.
';
}
$payload = base64_encode($json);
$html = '';
$html .= '
Φόρτωση Business Grid…Αν το μήνυμα παραμένει, το JavaScript του plugin δεν φορτώθηκε.
';
$html .= '
';
$html .= '';
if (!empty($row['custom_css'])) {
$html .= '';
}
return $html;
}
public static function ajax_save_workspace(): void {
self::require_capability();
check_ajax_referer('mrbb_workspace', 'nonce');
$app_id = isset($_POST['app_id']) ? absint($_POST['app_id']) : 0;
$row = self::get_workspace_row($app_id);
if (!$row) {
wp_send_json_error(['message' => 'Η εφαρμογή δεν βρέθηκε.'], 404);
}
$raw = isset($_POST['workspace']) ? wp_unslash((string) $_POST['workspace']) : '';
if ($raw === '' || strlen($raw) > self::MAX_CONFIG_BYTES) {
wp_send_json_error(['message' => 'Τα δεδομένα της εφαρμογής είναι κενά ή υπερβολικά μεγάλα.'], 422);
}
$workspace = json_decode($raw, true);
if (!is_array($workspace)) {
wp_send_json_error(['message' => 'Μη έγκυρη μορφή δεδομένων.'], 422);
}
$workspace = self::sanitize_workspace($workspace);
$name = isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : (string) $row['name'];
$name = $name !== '' ? $name : 'Η επιχείρησή μου';
$description = isset($_POST['description']) ? sanitize_textarea_field(wp_unslash((string) $_POST['description'])) : (string) ($row['description'] ?? '');
$status = isset($_POST['status']) && wp_unslash((string) $_POST['status']) === 'published' ? 'published' : 'draft';
$custom_css = isset($_POST['custom_css']) ? wp_unslash((string) $_POST['custom_css']) : '';
$custom_js = current_user_can('unfiltered_html') && isset($_POST['custom_js']) ? wp_unslash((string) $_POST['custom_js']) : (string) ($row['custom_js'] ?? '');
global $wpdb;
$updated = $wpdb->update(
self::apps_table(),
[
'name' => $name,
'slug' => self::unique_slug($name, $app_id),
'description' => $description,
'config' => wp_json_encode($workspace, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'custom_css' => $custom_css,
'custom_js' => $custom_js,
'status' => $status,
'updated_at' => current_time('mysql'),
],
['id' => $app_id],
['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'],
['%d']
);
if ($updated === false) {
wp_send_json_error(['message' => 'Αποτυχία αποθήκευσης στη βάση.'], 500);
}
wp_send_json_success([
'message' => 'Οι αλλαγές αποθηκεύτηκαν.',
'updatedAt' => current_time('mysql'),
]);
}
public static function ajax_create_workspace(): void {
self::require_capability();
check_ajax_referer('mrbb_workspace', 'nonce');
$name = isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : 'Νέα επιχείρηση';
$name = $name !== '' ? $name : 'Νέα επιχείρηση';
$now = current_time('mysql');
global $wpdb;
$ok = $wpdb->insert(
self::apps_table(),
[
'name' => $name,
'slug' => self::unique_slug($name),
'description' => '',
'config' => wp_json_encode(self::default_workspace(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'custom_css' => '',
'custom_js' => '',
'status' => 'draft',
'created_by' => get_current_user_id(),
'created_at' => $now,
'updated_at' => $now,
],
['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s']
);
if (!$ok) {
wp_send_json_error(['message' => 'Δεν δημιουργήθηκε ο νέος χώρος.'], 500);
}
wp_send_json_success(['id' => (int) $wpdb->insert_id]);
}
public static function ajax_delete_workspace(): void {
self::require_capability();
check_ajax_referer('mrbb_workspace', 'nonce');
$app_id = isset($_POST['app_id']) ? absint($_POST['app_id']) : 0;
if ($app_id <= 0) {
wp_send_json_error(['message' => 'Λάθος εφαρμογή.'], 422);
}
global $wpdb;
$count = (int) $wpdb->get_var('SELECT COUNT(*) FROM ' . self::apps_table());
if ($count <= 1) {
wp_send_json_error(['message' => 'Δεν μπορεί να διαγραφεί η μοναδική εφαρμογή.'], 422);
}
$wpdb->delete(self::apps_table(), ['id' => $app_id], ['%d']);
$wpdb->delete(self::records_table(), ['app_id' => $app_id], ['%d']);
wp_send_json_success(['message' => 'Η εφαρμογή διαγράφηκε.']);
}
private static function sanitize_workspace(array $workspace): array {
$allowed_types = ['text', 'number', 'currency', 'date', 'select', 'status', 'checkbox', 'formula', 'note', 'kpi', 'progress', 'chart', 'button'];
$clean = [
'schemaVersion' => 3,
'theme' => [
'accent' => sanitize_hex_color((string) ($workspace['theme']['accent'] ?? '#6d5dfc')) ?: '#6d5dfc',
'accentSoft' => sanitize_hex_color((string) ($workspace['theme']['accentSoft'] ?? '#edeaff')) ?: '#edeaff',
'background' => sanitize_hex_color((string) ($workspace['theme']['background'] ?? '#f5f7fb')) ?: '#f5f7fb',
'surface' => sanitize_hex_color((string) ($workspace['theme']['surface'] ?? '#ffffff')) ?: '#ffffff',
'text' => sanitize_hex_color((string) ($workspace['theme']['text'] ?? '#1f2937')) ?: '#1f2937',
'muted' => sanitize_hex_color((string) ($workspace['theme']['muted'] ?? '#667085')) ?: '#667085',
'grid' => sanitize_hex_color((string) ($workspace['theme']['grid'] ?? '#dfe3eb')) ?: '#dfe3eb',
'header' => sanitize_hex_color((string) ($workspace['theme']['header'] ?? '#f1f3f8')) ?: '#f1f3f8',
'radius' => max(0, min(24, (int) ($workspace['theme']['radius'] ?? 10))),
],
'activeSceneId' => sanitize_key((string) ($workspace['activeSceneId'] ?? 'clients')),
'scenes' => [],
];
$scenes = is_array($workspace['scenes'] ?? null) ? array_slice($workspace['scenes'], 0, 20) : [];
foreach ($scenes as $scene_index => $scene) {
if (!is_array($scene)) {
continue;
}
$rows = max(10, min(250, (int) ($scene['rows'] ?? 50)));
$columns = max(5, min(52, (int) ($scene['columns'] ?? 12)));
$scene_id = sanitize_key((string) ($scene['id'] ?? ('scene-' . ($scene_index + 1))));
$scene_id = $scene_id !== '' ? $scene_id : 'scene-' . ($scene_index + 1);
$clean_scene = [
'id' => $scene_id,
'title' => sanitize_text_field((string) ($scene['title'] ?? ('Σκηνή ' . ($scene_index + 1)))),
'icon' => sanitize_text_field((string) ($scene['icon'] ?? '▦')),
'rows' => $rows,
'columns' => $columns,
'frozenRows' => max(0, min(5, (int) ($scene['frozenRows'] ?? 1))),
'frozenColumns' => max(0, min(5, (int) ($scene['frozenColumns'] ?? 1))),
'columnWidths' => [],
'rowHeights' => [],
'cells' => [],
'merges' => [],
];
foreach ((array) ($scene['columnWidths'] ?? []) as $column => $width) {
if (preg_match('/^[A-Z]{1,2}$/', (string) $column)) {
$clean_scene['columnWidths'][(string) $column] = max(60, min(520, (int) $width));
}
}
foreach ((array) ($scene['rowHeights'] ?? []) as $row => $height) {
$row_number = (int) $row;
if ($row_number >= 1 && $row_number <= $rows) {
$clean_scene['rowHeights'][(string) $row_number] = max(26, min(260, (int) $height));
}
}
$cells = is_array($scene['cells'] ?? null) ? array_slice($scene['cells'], 0, 25000, true) : [];
foreach ($cells as $address => $cell) {
if (!preg_match('/^[A-Z]{1,2}[1-9][0-9]{0,2}$/', (string) $address) || !is_array($cell)) {
continue;
}
$type = in_array((string) ($cell['type'] ?? 'text'), $allowed_types, true) ? (string) $cell['type'] : 'text';
$clean_cell = [
'type' => $type,
'value' => self::sanitize_cell_value($cell['value'] ?? ''),
];
if (isset($cell['formula'])) {
$clean_cell['formula'] = mb_substr(trim((string) $cell['formula']), 0, 1000);
}
if (isset($cell['label'])) {
$clean_cell['label'] = sanitize_text_field((string) $cell['label']);
}
if (isset($cell['options']) && is_array($cell['options'])) {
$clean_cell['options'] = array_values(array_filter(array_map(static fn($item) => sanitize_text_field((string) $item), array_slice($cell['options'], 0, 100)), static fn($item) => $item !== ''));
}
if (isset($cell['config']) && is_array($cell['config'])) {
$clean_cell['config'] = [
'sourceRange' => sanitize_text_field((string) ($cell['config']['sourceRange'] ?? '')),
'labelRange' => sanitize_text_field((string) ($cell['config']['labelRange'] ?? '')),
'max' => max(1, (float) ($cell['config']['max'] ?? 100)),
'url' => esc_url_raw((string) ($cell['config']['url'] ?? '')),
'buttonLabel' => sanitize_text_field((string) ($cell['config']['buttonLabel'] ?? 'Άνοιγμα')),
];
}
if (isset($cell['style']) && is_array($cell['style'])) {
$style = $cell['style'];
$clean_cell['style'] = [
'background' => sanitize_hex_color((string) ($style['background'] ?? '')) ?: '',
'color' => sanitize_hex_color((string) ($style['color'] ?? '')) ?: '',
'align' => in_array((string) ($style['align'] ?? 'left'), ['left', 'center', 'right'], true) ? (string) $style['align'] : 'left',
'bold' => !empty($style['bold']),
'italic' => !empty($style['italic']),
'fontSize' => max(9, min(40, (int) ($style['fontSize'] ?? 13))),
'borderColor' => sanitize_hex_color((string) ($style['borderColor'] ?? '')) ?: '',
];
}
$clean_scene['cells'][(string) $address] = $clean_cell;
}
foreach (array_slice((array) ($scene['merges'] ?? []), 0, 500) as $merge) {
if (!is_array($merge)) {
continue;
}
$start = strtoupper(sanitize_text_field((string) ($merge['start'] ?? '')));
$end = strtoupper(sanitize_text_field((string) ($merge['end'] ?? '')));
if (preg_match('/^[A-Z]{1,2}[1-9][0-9]{0,2}$/', $start) && preg_match('/^[A-Z]{1,2}[1-9][0-9]{0,2}$/', $end)) {
$clean_scene['merges'][] = ['start' => $start, 'end' => $end];
}
}
$clean['scenes'][] = $clean_scene;
}
if (!$clean['scenes']) {
return self::default_workspace();
}
if (self::scene_index($clean, $clean['activeSceneId']) < 0) {
$clean['activeSceneId'] = (string) $clean['scenes'][0]['id'];
}
return $clean;
}
private static function sanitize_cell_value(mixed $value): mixed {
if (is_bool($value) || is_int($value) || is_float($value) || $value === null) {
return $value;
}
if (is_array($value)) {
return array_values(array_map([__CLASS__, 'sanitize_cell_value'], array_slice($value, 0, 100)));
}
return mb_substr(wp_strip_all_tags((string) $value), 0, 5000);
}
private static function strings(): array {
return [
'saved' => 'Αποθηκεύτηκε',
'saving' => 'Αποθήκευση…',
'unsaved' => 'Μη αποθηκευμένες αλλαγές',
'confirmDeleteScene' => 'Να διαγραφεί αυτή η σκηνή;',
'confirmDeleteApp' => 'Να διαγραφεί ολόκληρη η εφαρμογή;',
'pasteHint' => 'Αντιγραφή από Google Sheets ή Excel και επικόλληση στο επιλεγμένο κελί.',
];
}
private static function cell(mixed $value = '', string $type = 'text', array $extra = []): array {
return array_merge(['type' => $type, 'value' => $value], $extra);
}
private static function header_cell(string $value): array {
return self::cell($value, 'text', [
'style' => [
'background' => '#eef1f8',
'color' => '#344054',
'align' => 'center',
'bold' => true,
'fontSize' => 12,
'borderColor' => '#d5dae6',
],
]);
}
private static function default_workspace(): array {
$months = ['Σεπτέμβριος', 'Οκτώβριος', 'Νοέμβριος', 'Δεκέμβριος', 'Ιανουάριος', 'Φεβρουάριος', 'Μάρτιος', 'Απρίλιος', 'Μάιος', 'Ιούνιος', 'Ιούλιος'];
$clients = self::clients_scene($months);
$employees = self::employees_scene();
$schedule = self::schedule_scene();
$finance = self::finance_scene($months);
return [
'schemaVersion' => 3,
'activeSceneId' => 'clients',
'theme' => [
'accent' => '#6d5dfc',
'accentSoft' => '#edeaff',
'background' => '#f5f7fb',
'surface' => '#ffffff',
'text' => '#1f2937',
'muted' => '#667085',
'grid' => '#dfe3eb',
'header' => '#f1f3f8',
'radius' => 10,
],
'scenes' => [$clients, $employees, $schedule, $finance],
];
}
private static function clients_scene(array $months): array {
$cells = [];
$headers = ['Α/Α', 'Πελάτης', 'Τηλέφωνο', 'Υπηρεσία / Όργανο', 'Μηνιαία χρέωση', 'Πληρωμή', 'Υπόλοιπο', 'Τρόπος πληρωμής', 'Μήνας', 'Κατάσταση', 'Ημερομηνία', 'Σημειώσεις'];
foreach ($headers as $index => $header) {
$cells[self::column_name($index + 1) . '1'] = self::header_cell($header);
}
$payment_methods = ['Μετρητά', 'POS', 'IRIS', 'Τραπεζική κατάθεση', 'Viva / Stripe', 'Άλλο'];
for ($row = 2; $row <= 80; $row++) {
$cells['A' . $row] = self::cell('=ROW()-1', 'formula', ['formula' => '=ROW()-1']);
$cells['E' . $row] = self::cell('', 'currency');
$cells['F' . $row] = self::cell('', 'currency');
$cells['G' . $row] = self::cell('', 'formula', ['formula' => '=E' . $row . '-F' . $row]);
$cells['H' . $row] = self::cell('', 'select', ['options' => $payment_methods]);
$cells['I' . $row] = self::cell('', 'select', ['options' => $months]);
$cells['J' . $row] = self::cell('', 'formula', ['formula' => '=IF(G' . $row . '<=0,"Πληρωμένο",IF(F' . $row . '>0,"Μερική","Οφειλή"))']);
$cells['K' . $row] = self::cell('', 'date');
}
return [
'id' => 'clients',
'title' => 'Πελάτες - Πληρωμές',
'icon' => '◉',
'rows' => 80,
'columns' => 12,
'frozenRows' => 1,
'frozenColumns' => 2,
'columnWidths' => [
'A' => 64, 'B' => 230, 'C' => 145, 'D' => 180, 'E' => 130, 'F' => 120,
'G' => 120, 'H' => 165, 'I' => 145, 'J' => 125, 'K' => 135, 'L' => 260,
],
'rowHeights' => ['1' => 42],
'cells' => $cells,
'merges' => [],
];
}
private static function employees_scene(): array {
$cells = [];
$headers = ['Α/Α', 'Υπάλληλος', 'Ρόλος', 'Τηλέφωνο', 'Email', 'Αμοιβή / ώρα', 'Ώρες μήνα', 'Αμοιβή μήνα', 'Κατάσταση', 'Σημειώσεις'];
foreach ($headers as $index => $header) {
$cells[self::column_name($index + 1) . '1'] = self::header_cell($header);
}
for ($row = 2; $row <= 60; $row++) {
$cells['A' . $row] = self::cell('', 'formula', ['formula' => '=ROW()-1']);
$cells['F' . $row] = self::cell('', 'currency');
$cells['G' . $row] = self::cell('', 'number');
$cells['H' . $row] = self::cell('', 'formula', ['formula' => '=F' . $row . '*G' . $row]);
$cells['I' . $row] = self::cell('', 'status', ['options' => ['Ενεργός', 'Άδεια', 'Ανενεργός']]);
}
return [
'id' => 'employees',
'title' => 'Υπάλληλοι',
'icon' => '◇',
'rows' => 60,
'columns' => 10,
'frozenRows' => 1,
'frozenColumns' => 2,
'columnWidths' => [
'A' => 64, 'B' => 220, 'C' => 160, 'D' => 145, 'E' => 220,
'F' => 125, 'G' => 115, 'H' => 140, 'I' => 125, 'J' => 260,
],
'rowHeights' => ['1' => 42],
'cells' => $cells,
'merges' => [],
];
}
private static function schedule_scene(): array {
$cells = [];
$headers = ['Ώρα', 'Δευτέρα', 'Τρίτη', 'Τετάρτη', 'Πέμπτη', 'Παρασκευή', 'Σάββατο', 'Αίθουσα / Σημείωση'];
foreach ($headers as $index => $header) {
$cells[self::column_name($index + 1) . '1'] = self::header_cell($header);
}
$row = 2;
for ($hour = 10; $hour <= 22; $hour++) {
foreach (['00', '30'] as $minutes) {
$cells['A' . $row] = self::cell(sprintf('%02d:%s', $hour, $minutes), 'text', [
'style' => [
'background' => '#f7f8fb', 'color' => '#475467', 'align' => 'center',
'bold' => true, 'fontSize' => 12, 'borderColor' => '#dfe3eb',
],
]);
$row++;
if ($row > 28) {
break 2;
}
}
}
return [
'id' => 'schedule',
'title' => 'Πρόγραμμα',
'icon' => '▦',
'rows' => 28,
'columns' => 8,
'frozenRows' => 1,
'frozenColumns' => 1,
'columnWidths' => [
'A' => 90, 'B' => 220, 'C' => 220, 'D' => 220, 'E' => 220,
'F' => 220, 'G' => 220, 'H' => 230,
],
'rowHeights' => ['1' => 42],
'cells' => $cells,
'merges' => [],
];
}
private static function finance_scene(array $months): array {
$cells = [];
$cells['A1'] = self::header_cell('Κατηγορία');
foreach ($months as $index => $month) {
$cells[self::column_name($index + 2) . '1'] = self::header_cell($month);
}
$cells['M1'] = self::header_cell('Ετήσιο');
$labels = [
2 => 'Σύνολο πληρωμών',
3 => 'Μετρητά',
4 => 'Online πληρωμές',
5 => 'Έξοδα',
6 => 'Καθαρό αποτέλεσμα',
];
foreach ($labels as $row => $label) {
$cells['A' . $row] = self::cell($label, 'text', [
'style' => [
'background' => $row === 6 ? '#ecfdf3' : '#f8f9fc',
'color' => $row === 6 ? '#067647' : '#344054',
'align' => 'left', 'bold' => true, 'fontSize' => 12, 'borderColor' => '#dfe3eb',
],
]);
}
foreach ($months as $index => $month) {
$column = self::column_name($index + 2);
$escaped = str_replace('"', '\\"', $month);
$cells[$column . '2'] = self::cell('', 'formula', ['formula' => '=SCENE_SUMIF("clients","I2:I80","' . $escaped . '","F2:F80")']);
$cells[$column . '3'] = self::cell('', 'formula', ['formula' => '=SCENE_SUMIFS("clients","F2:F80","I2:I80","' . $escaped . '","H2:H80","Μετρητά")']);
$cells[$column . '4'] = self::cell('', 'formula', ['formula' => '=' . $column . '2-' . $column . '3']);
$cells[$column . '5'] = self::cell('', 'currency');
$cells[$column . '6'] = self::cell('', 'formula', ['formula' => '=' . $column . '2-' . $column . '5']);
}
foreach ([2, 3, 4, 5, 6] as $row) {
$cells['M' . $row] = self::cell('', 'formula', ['formula' => '=SUM(B' . $row . ':L' . $row . ')']);
}
$cells['A9'] = self::cell('', 'kpi', [
'label' => 'Ετήσια έσοδα',
'formula' => '=M2',
'style' => ['background' => '#ffffff', 'color' => '#1f2937', 'align' => 'left', 'bold' => false, 'fontSize' => 13, 'borderColor' => '#d9d6ff'],
]);
$cells['E9'] = self::cell('', 'kpi', [
'label' => 'Ετήσια έξοδα',
'formula' => '=M5',
'style' => ['background' => '#ffffff', 'color' => '#1f2937', 'align' => 'left', 'bold' => false, 'fontSize' => 13, 'borderColor' => '#d9d6ff'],
]);
$cells['I9'] = self::cell('', 'chart', [
'label' => 'Καθαρό αποτέλεσμα ανά μήνα',
'config' => ['sourceRange' => 'B6:L6', 'labelRange' => 'B1:L1', 'max' => 100, 'url' => '', 'buttonLabel' => ''],
'style' => ['background' => '#ffffff', 'color' => '#1f2937', 'align' => 'left', 'bold' => false, 'fontSize' => 13, 'borderColor' => '#d9d6ff'],
]);
return [
'id' => 'finance',
'title' => 'Έσοδα - Έξοδα',
'icon' => '◫',
'rows' => 24,
'columns' => 13,
'frozenRows' => 1,
'frozenColumns' => 1,
'columnWidths' => [
'A' => 220, 'B' => 125, 'C' => 125, 'D' => 125, 'E' => 125, 'F' => 125,
'G' => 125, 'H' => 125, 'I' => 125, 'J' => 125, 'K' => 125, 'L' => 125, 'M' => 140,
],
'rowHeights' => ['1' => 42, '9' => 48, '10' => 48, '11' => 48, '12' => 48],
'cells' => $cells,
'merges' => [
['start' => 'A9', 'end' => 'D12'],
['start' => 'E9', 'end' => 'H12'],
['start' => 'I9', 'end' => 'M12'],
],
];
}
private static function column_name(int $number): string {
$name = '';
while ($number > 0) {
$number--;
$name = chr(65 + ($number % 26)) . $name;
$number = intdiv($number, 26);
}
return $name;
}
}
MRBB_Business_Grid::boot();
https://musicroad.gr/page-sitemap.xml
2026-07-20T10:01:27+00:00
https://musicroad.gr/category-sitemap.xml