/** * 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 '
Φόρτωση Business Grid…
'; 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