/**
* 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();
Μαθήματα Μουσικής από 20€/μήνα Καλλιθέα | Περιστέρι
Μετάβαση στο περιεχόμενο
Music Road • Καλλιτεχνική εκπαίδευση
Η μουσική αποκτά σπίτι.
Κλασική παιδεία. Σύγχρονη εμπειρία.
Καλλιθέα • Περιστέρι
Παιδιά & ενήλικες
Ατομικά & ομαδικά
Κλασικό & σύγχρονο
Μάθημα & σκηνή
Οι διαδρομές σου
Βρες τον δικό σου ήχο.
Επίλεξε το μάθημα που σε εκφράζει και ξεκίνα μια διαδρομή με βάση, δημιουργικότητα και προσωπικό στόχο.
Η φιλοσοφία μας
Παράδοση στη βάση.
Μέλλον στην εμπειρία.
Στο Music Road η τεχνολογία δεν αντικαθιστά τον δάσκαλο. Ενισχύει τη διδασκαλία, κάνει τη γνώση πιο ζωντανή και ανοίγει νέους δρόμους δημιουργίας.
Γνώρισε το Music Road →
01
Προσωπική διαδρομή
Το μάθημα προσαρμόζεται στην ηλικία, στους στόχους και στον ρυθμό κάθε μαθητή.
02
Κλασική βάση
Στέρεη μουσική παιδεία, σωστή τεχνική και ουσιαστική κατανόηση της μουσικής.
03
Σύγχρονη εμπειρία
Διαδραστικά εργαλεία, τεχνολογία και δημιουργικές εφαρμογές μέσα στη διδασκαλία.
04
Ζωντανή μουσική
Σύνολα, δράσεις και εμπειρίες που μεταφέρουν τη μουσική από την αίθουσα στη σκηνή.
Οι χώροι μας
Χώροι που σε καλούν
να δημιουργήσεις.
Ζεστά υλικά, σύγχρονος εξοπλισμός και αίθουσες σχεδιασμένες για συγκέντρωση, συνεργασία και έμπνευση.
Welcome
Οικεία ατμόσφαιρα
Ένας χώρος ανοιχτός σε μαθητές και οικογένειες.
Εξοπλισμός
Ο ήχος στο κέντρο
Ένας χώρος όπου το μάθημα γίνεται εμπειρία.
Technology
Διακριτική τεχνολογία
Εργαλεία που υπηρετούν τη μουσική και τη μάθηση.
Η εμπειρία Music Road
Δεν μαθαίνουμε απλώς μουσική.
Τη ζούμε.
01
Μαθαίνω
Χτίζω τεχνική και μουσική βάση με καθαρή καθοδήγηση.
02
Δημιουργώ
Εξερευνώ ήχο, ρεπερτόριο, αυτοσχεδιασμό και προσωπική έκφραση.
03
Συνεργάζομαι
Παίζω με άλλους, ακούω, συντονίζομαι και εξελίσσομαι.
04
Παρουσιάζω
Η γνώση γίνεται αυτοπεποίθηση, εμπειρία και πραγματική μουσική παρουσία.
Δύο σημεία στην πόλη
Διάλεξε το δικό σου Music Road.
Η κοινότητά μας
Τι λένε οι μαθητές μας.
“
Εξαιρετικοί δάσκαλοι, υπέροχο περιβάλλον και πραγματική αγάπη για τη μουσική.
★★★★★
Μαρία Κ.
“
Από την πρώτη στιγμή ένιωσα πως είμαι στο σωστό μέρος για να εξελιχθώ.
★★★★★
Γιώργος Π.
“
Το Music Road έγινε το δεύτερό μας σπίτι. Εδώ η μουσική έχει χώρο να ανθίσει.
★★★★★
Ελένη Τ.
Συχνές ερωτήσεις
Όσα χρειάζεσαι
πριν ξεκινήσεις.
Δεν βρήκες αυτό που ψάχνεις; Επικοινώνησε μαζί μας και θα σε καθοδηγήσουμε προσωπικά.
Επικοινωνία →
Από ποια ηλικία ξεκινούν τα μαθήματα;
Η κατάλληλη ηλικία εξαρτάται από το μάθημα και το παιδί. Επικοινώνησε μαζί μας για να προτείνουμε το σωστό ξεκίνημα.
Υπάρχουν μαθήματα και για ενήλικες;
Ναι. Υπάρχουν προγράμματα για αρχάριους και προχωρημένους ενήλικες, με στόχους που προσαρμόζονται στον διαθέσιμο χρόνο τους.
Χρειάζεται να έχω δικό μου όργανο;
Όχι απαραίτητα από το πρώτο μάθημα. Θα σε καθοδηγήσουμε για το πότε και ποιο όργανο είναι κατάλληλο να αποκτήσεις.
Μπορώ να κάνω δοκιμαστικό μάθημα;
Ναι. Επίλεξε παράρτημα, δες το διαθέσιμο πρόγραμμα και κλείσε το δοκιμαστικό σου μάθημα.
Σε ποιο παράρτημα προσφέρεται κάθε μάθημα;
Η διαθεσιμότητα διαφοροποιείται ανά παράρτημα. Από τα κουμπιά προγράμματος βλέπεις τις ημέρες και ώρες για Καλλιθέα και Περιστέρι.
Το επόμενο βήμα
Ο δικός σου δρόμος
ξεκινά με μία νότα.
Δες το πρόγραμμα του παραρτήματος που σε εξυπηρετεί και κλείσε το πρώτο σου δοκιμαστικό μάθημα.