From 6b9021084a562bf534e5f4914ba8de6d87a61468 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 6 Nov 2025 09:44:40 +0000 Subject: [PATCH] 1.0 --- .perm_test_apache | 0 .perm_test_exec | 0 agents.php | 6 + ai/LocalAIApi.php | 311 +++++++++++++++++++++++++++++++++++++++++++ ai/config.php | 52 ++++++++ assets/css/style.css | 29 ++++ clients.php | 6 + database.sql | 128 ++++++++++++++++++ db/config.php | 32 +++-- footer.php | 10 ++ header.php | 65 +++++++++ index.php | 235 +++++++++++++------------------- owners.php | 6 + properties.php | 6 + transactions.php | 6 + 15 files changed, 733 insertions(+), 159 deletions(-) create mode 100644 .perm_test_apache create mode 100644 .perm_test_exec create mode 100644 agents.php create mode 100644 ai/LocalAIApi.php create mode 100644 ai/config.php create mode 100644 assets/css/style.css create mode 100644 clients.php create mode 100644 database.sql create mode 100644 footer.php create mode 100644 header.php create mode 100644 owners.php create mode 100644 properties.php create mode 100644 transactions.php diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/agents.php b/agents.php new file mode 100644 index 0000000..991dfae --- /dev/null +++ b/agents.php @@ -0,0 +1,6 @@ + + +

Manage Agents

+

This section is under construction.

+ + diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php new file mode 100644 index 0000000..00b1b00 --- /dev/null +++ b/ai/LocalAIApi.php @@ -0,0 +1,311 @@ + [ +// ['role' => 'system', 'content' => 'You are a helpful assistant.'], +// ['role' => 'user', 'content' => 'Tell me a bedtime story.'], +// ], +// ]); +// if (!empty($response['success'])) { +// $decoded = LocalAIApi::decodeJsonFromResponse($response); +// } + +class LocalAIApi +{ + /** @var array|null */ + private static ?array $configCache = null; + + /** + * Signature compatible with the OpenAI Responses API. + * + * @param array $params Request body (model, input, text, reasoning, metadata, etc.). + * @param array $options Extra options (timeout, verify_tls, headers, path, project_uuid). + * @return array{ + * success:bool, + * status?:int, + * data?:mixed, + * error?:string, + * response?:mixed, + * message?:string + * } + */ + public static function createResponse(array $params, array $options = []): array + { + $cfg = self::config(); + $payload = $params; + + if (empty($payload['input']) || !is_array($payload['input'])) { + return [ + 'success' => false, + 'error' => 'input_missing', + 'message' => 'Parameter "input" is required and must be an array.', + ]; + } + + if (!isset($payload['model']) || $payload['model'] === '') { + $payload['model'] = $cfg['default_model']; + } + + return self::request($options['path'] ?? null, $payload, $options); + } + + /** + * Snake_case alias for createResponse (matches the provided example). + * + * @param array $params + * @param array $options + * @return array + */ + public static function create_response(array $params, array $options = []): array + { + return self::createResponse($params, $options); + } + + /** + * Perform a raw request to the AI proxy. + * + * @param string $path Endpoint (may be an absolute URL). + * @param array $payload JSON payload. + * @param array $options Additional request options. + * @return array + */ + public static function request(?string $path = null, array $payload = [], array $options = []): array + { + if (!function_exists('curl_init')) { + return [ + 'success' => false, + 'error' => 'curl_missing', + 'message' => 'PHP cURL extension is missing. Install or enable it on the VM.', + ]; + } + + $cfg = self::config(); + + $projectUuid = $cfg['project_uuid']; + if (empty($projectUuid)) { + return [ + 'success' => false, + 'error' => 'project_uuid_missing', + 'message' => 'PROJECT_UUID is not defined; aborting AI request.', + ]; + } + + $defaultPath = $cfg['responses_path'] ?? null; + $resolvedPath = $path ?? ($options['path'] ?? $defaultPath); + if (empty($resolvedPath)) { + return [ + 'success' => false, + 'error' => 'project_id_missing', + 'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', + ]; + } + + $url = self::buildUrl($resolvedPath, $cfg['base_url']); + $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; + if ($timeout <= 0) { + $timeout = 30; + } + + $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; + $verifyTls = array_key_exists('verify_tls', $options) + ? (bool) $options['verify_tls'] + : $baseVerifyTls; + + $projectHeader = $cfg['project_header']; + + $headers = [ + 'Content-Type: application/json', + 'Accept: application/json', + ]; + $headers[] = $projectHeader . ': ' . $projectUuid; + if (!empty($options['headers']) && is_array($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && $header !== '') { + $headers[] = $header; + } + } + } + + if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) { + $payload['project_uuid'] = $projectUuid; + } + + $body = json_encode($payload, JSON_UNESCAPED_UNICODE); + if ($body === false) { + return [ + 'success' => false, + 'error' => 'json_encode_failed', + 'message' => 'Failed to encode request body to JSON.', + ]; + } + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + + $responseBody = curl_exec($ch); + if ($responseBody === false) { + $error = curl_error($ch) ?: 'Unknown cURL error'; + curl_close($ch); + return [ + 'success' => false, + 'error' => 'curl_error', + 'message' => $error, + ]; + } + + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $decoded = null; + if ($responseBody !== '' && $responseBody !== null) { + $decoded = json_decode($responseBody, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $decoded = null; + } + } + + if ($status >= 200 && $status < 300) { + return [ + 'success' => true, + 'status' => $status, + 'data' => $decoded ?? $responseBody, + ]; + } + + $errorMessage = 'AI proxy request failed'; + if (is_array($decoded)) { + $errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage; + } elseif (is_string($responseBody) && $responseBody !== '') { + $errorMessage = $responseBody; + } + + return [ + 'success' => false, + 'status' => $status, + 'error' => $errorMessage, + 'response' => $decoded ?? $responseBody, + ]; + } + + /** + * Extract plain text from a Responses API payload. + * + * @param array $response Result of LocalAIApi::createResponse|request. + * @return string + */ + public static function extractText(array $response): string + { + $payload = $response['data'] ?? $response; + if (!is_array($payload)) { + return ''; + } + + if (!empty($payload['output']) && is_array($payload['output'])) { + $combined = ''; + foreach ($payload['output'] as $item) { + if (!isset($item['content']) || !is_array($item['content'])) { + continue; + } + foreach ($item['content'] as $block) { + if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) { + $combined .= $block['text']; + } + } + } + if ($combined !== '') { + return $combined; + } + } + + if (!empty($payload['choices'][0]['message']['content'])) { + return (string) $payload['choices'][0]['message']['content']; + } + + return ''; + } + + /** + * Attempt to decode JSON emitted by the model (handles markdown fences). + * + * @param array $response + * @return array|null + */ + public static function decodeJsonFromResponse(array $response): ?array + { + $text = self::extractText($response); + if ($text === '') { + return null; + } + + $decoded = json_decode($text, true); + if (is_array($decoded)) { + return $decoded; + } + + $stripped = preg_replace('/^```json|```$/m', '', trim($text)); + if ($stripped !== null && $stripped !== $text) { + $decoded = json_decode($stripped, true); + if (is_array($decoded)) { + return $decoded; + } + } + + return null; + } + + /** + * Load configuration from ai/config.php. + * + * @return array + */ + private static function config(): array + { + if (self::$configCache === null) { + $configPath = __DIR__ . '/config.php'; + if (!file_exists($configPath)) { + throw new RuntimeException('AI config file not found: ai/config.php'); + } + $cfg = require $configPath; + if (!is_array($cfg)) { + throw new RuntimeException('Invalid AI config format: expected array'); + } + self::$configCache = $cfg; + } + + return self::$configCache; + } + + /** + * Build an absolute URL from base_url and a path. + */ + private static function buildUrl(string $path, string $baseUrl): string + { + $trimmed = trim($path); + if ($trimmed === '') { + return $baseUrl; + } + if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { + return $trimmed; + } + if ($trimmed[0] === '/') { + return $baseUrl . $trimmed; + } + return $baseUrl . '/' . $trimmed; + } +} + +// Legacy alias for backward compatibility with the previous class name. +if (!class_exists('OpenAIService')) { + class_alias(LocalAIApi::class, 'OpenAIService'); +} diff --git a/ai/config.php b/ai/config.php new file mode 100644 index 0000000..1ba1596 --- /dev/null +++ b/ai/config.php @@ -0,0 +1,52 @@ + $baseUrl, + 'responses_path' => $responsesPath, + 'project_id' => $projectId, + 'project_uuid' => $projectUuid, + 'project_header' => 'project-uuid', + 'default_model' => 'gpt-5', + 'timeout' => 30, + 'verify_tls' => true, +]; diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..95c355e --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,29 @@ +body { + background-color: #f8f9fa; +} + +.navbar { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.card { + border: none; + border-radius: 0.5rem; + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.1); +} + +.card-title { + font-weight: 600; +} + +.stat-number { + font-size: 2.5rem; + font-weight: 700; + color: #0d6efd; +} diff --git a/clients.php b/clients.php new file mode 100644 index 0000000..2e782b4 --- /dev/null +++ b/clients.php @@ -0,0 +1,6 @@ + + +

Manage Clients

+

This section is under construction.

+ + diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..54de36c --- /dev/null +++ b/database.sql @@ -0,0 +1,128 @@ +-- Create user and grant privileges +DROP USER IF EXISTS 'app_user'@'localhost'; +CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'Secure@Password123'; +GRANT ALL PRIVILEGES ON real_estate.* TO 'app_user'@'localhost'; +FLUSH PRIVILEGES; + +-- Create the database +CREATE DATABASE IF NOT EXISTS `real_estate`; +USE `real_estate`; + +-- Table structure for table `owner` +CREATE TABLE IF NOT EXISTS `owner` ( + `owner_id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `contact` VARCHAR(20) UNIQUE, + `email` VARCHAR(255) UNIQUE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Inserting 5 sample records into `owner` +INSERT INTO `owner` (`name`, `contact`, `email`) VALUES +('John Smith', '111-222-3333', 'john.smith@example.com'), +('Jane Doe', '444-555-6666', 'jane.doe@example.com'), +('Peter Jones', '777-888-9999', 'peter.jones@example.com'), +('Mary Williams', '123-456-7890', 'mary.williams@example.com'), +('David Brown', '098-765-4321', 'david.brown@example.com'); + +-- Table structure for table `agent` +CREATE TABLE IF NOT EXISTS `agent` ( + `agent_id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `contact` VARCHAR(20) UNIQUE, + `email` VARCHAR(255) UNIQUE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Inserting 5 sample records into `agent` +INSERT INTO `agent` (`name`, `contact`, `email`) VALUES +('Agent Alice', '101-202-3030', 'alice@realestate.com'), +('Agent Bob', '404-505-6060', 'bob@realestate.com'), +('Agent Charlie', '707-808-9090', 'charlie@realestate.com'), +('Agent Diana', '112-223-3344', 'diana@realestate.com'), +('Agent Eve', '556-667-7788', 'eve@realestate.com'); + +-- Table structure for table `clients` +CREATE TABLE IF NOT EXISTS `clients` ( + `client_id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `phone` VARCHAR(20) UNIQUE, + `email` VARCHAR(255) UNIQUE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Inserting 5 sample records into `clients` +INSERT INTO `clients` (`name`, `phone`, `email`) VALUES +('Client Chris', '121-232-3434', 'chris.c@client.com'), +('Client Dana', '454-565-6767', 'dana.d@client.com'), +('Client Frank', '787-898-9090', 'frank.f@client.com'), +('Client Grace', '212-323-4343', 'grace.g@client.com'), +('Client Heidi', '545-656-7676', 'heidi.h@client.com'); + +-- Table structure for table `properties` +CREATE TABLE IF NOT EXISTS `properties` ( + `property_id` INT AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(255) NOT NULL, + `type` VARCHAR(50) NOT NULL, + `address` VARCHAR(255) NOT NULL, + `city` VARCHAR(100) NOT NULL, + `price` DECIMAL(12, 2) NOT NULL, + `status` ENUM('Available', 'Sold', 'Rented') NOT NULL DEFAULT 'Available', + `owner_id` INT, + `agent_id` INT, + CONSTRAINT `fk_owner` FOREIGN KEY (`owner_id`) REFERENCES `owner` (`owner_id`) ON DELETE SET NULL, + CONSTRAINT `fk_agent` FOREIGN KEY (`agent_id`) REFERENCES `agent` (`agent_id`) ON DELETE SET NULL, + CHECK (`price` > 0) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Inserting 5 sample records into `properties` +INSERT INTO `properties` (`title`, `type`, `address`, `city`, `price`, `status`, `owner_id`, `agent_id`) VALUES +('Modern Downtown Loft', 'House', '123 Main St', 'Metropolis', 1200000.00, 'Available', 1, 1), +('Suburban Family Home', 'House', '456 Oak Ave', 'Smallville', 750000.00, 'Available', 2, 2), +('Luxury Beachfront Villa', 'House', '789 Ocean Dr', 'Coast City', 2500000.00, 'Sold', 3, 1), +('Cozy Countryside Cottage', 'House', '101 Pine Ln', 'Green Valley', 450000.00, 'Rented', 4, 3), +('High-Rise Apartment', 'Apartment', '212 Sky Blvd', 'Metropolis', 950000.00, 'Available', 5, 4); + +-- Table structure for table `transactions` +CREATE TABLE IF NOT EXISTS `transactions` ( + `transaction_id` INT AUTO_INCREMENT PRIMARY KEY, + `type` ENUM('Sale', 'Rent') NOT NULL, + `amount` DECIMAL(12, 2) NOT NULL, + `date` DATE NOT NULL, + `property_id` INT, + `agent_id` INT, + `client_id` INT, + UNIQUE (`property_id`), -- A property can only be in one final transaction + CONSTRAINT `fk_trans_property` FOREIGN KEY (`property_id`) REFERENCES `properties` (`property_id`) ON DELETE CASCADE, + CONSTRAINT `fk_trans_agent` FOREIGN KEY (`agent_id`) REFERENCES `agent` (`agent_id`) ON DELETE SET NULL, + CONSTRAINT `fk_trans_client` FOREIGN KEY (`client_id`) REFERENCES `clients` (`client_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Inserting 2 sample records into `transactions` +INSERT INTO `transactions` (`type`, `amount`, `date`, `property_id`, `agent_id`, `client_id`) VALUES +('Sale', 2450000.00, '2024-05-20', 3, 1, 3), +('Rent', 3000.00, '2024-06-01', 4, 3, 4); + +-- SQL Views +-- View for Available Properties +CREATE OR REPLACE VIEW `AvailableProperties` AS +SELECT `property_id`, `title`, `type`, `address`, `city`, `price` +FROM `properties` +WHERE `status` = 'Available'; + +-- View for High Value Properties +CREATE OR REPLACE VIEW `HighValueProperties` AS +SELECT `property_id`, `title`, `type`, `address`, `city`, `price` +FROM `properties` +WHERE `price` > 1000000; + +-- Trigger to update property status after a transaction +DELIMITER $$ +CREATE TRIGGER `after_transaction_insert` +AFTER INSERT ON `transactions` +FOR EACH ROW +BEGIN + IF NEW.type = 'Sale' THEN + UPDATE `properties` SET `status` = 'Sold' WHERE `property_id` = NEW.property_id; + ELSEIF NEW.type = 'Rent' THEN + UPDATE `properties` SET `status` = 'Rented' WHERE `property_id` = NEW.property_id; + END IF; +END$$ +DELIMITER ; \ No newline at end of file diff --git a/db/config.php b/db/config.php index f12ebaf..78ff593 100644 --- a/db/config.php +++ b/db/config.php @@ -1,17 +1,23 @@ PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); - } - return $pdo; +/** + * Establishes a database connection using PDO. + * @return PDO A PDO database connection object. + */ +function db_connect() { + try { + $pdoconn = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME, DB_USER, DB_PASS); + $pdoconn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + return $pdoconn; + } catch (PDOException $e) { + // In a real app, you'd log this error and show a generic message + die("Database connection failed: " . $e->getMessage()); + } } + +// You can now include this file and call db_connect() to get a database connection. \ No newline at end of file diff --git a/footer.php b/footer.php new file mode 100644 index 0000000..c1b1002 --- /dev/null +++ b/footer.php @@ -0,0 +1,10 @@ + + +
+

© Real Estate Management. Built with Flatlogic.

+
+ + + + + diff --git a/header.php b/header.php new file mode 100644 index 0000000..29f8b29 --- /dev/null +++ b/header.php @@ -0,0 +1,65 @@ + + + + + + Real Estate Management + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/index.php b/index.php index 7205f3d..e509681 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,93 @@ 0, + 'agents' => 0, + 'owners' => 0, + 'clients' => 0, + 'transactions' => 0 +]; + +try { + $counts['properties'] = $pdo->query('SELECT count(*) FROM properties')->fetchColumn(); + $counts['agents'] = $pdo->query('SELECT count(*) FROM agent')->fetchColumn(); + $counts['owners'] = $pdo->query('SELECT count(*) FROM owner')->fetchColumn(); + $counts['clients'] = $pdo->query('SELECT count(*) FROM clients')->fetchColumn(); + $counts['transactions'] = $pdo->query('SELECT count(*) FROM transactions')->fetchColumn(); +} catch (PDOException $e) { + // If the tables don't exist yet, we can just show 0. + // On a real site, you would log this error. +} -$phpVersion = PHP_VERSION; -$now = date('Y-m-d H:i:s'); ?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+

Admin Dashboard

+
+

Welcome to the Real Estate Management System. Here you can manage properties, clients, and transactions.

-
-
- Page updated: (UTC) -
- - + + +
+ +
+
+
+
Total Properties
+

+ Manage +
+
+
+ + +
+
+
+
Total Agents
+

+ Manage +
+
+
+ + +
+
+
+
Total Owners
+

+ Manage +
+
+
+ + +
+
+
+
Total Clients
+

+ Manage +
+
+
+ + +
+
+
+
Total Transactions
+

+ Manage +
+
+
+
+ + \ No newline at end of file diff --git a/owners.php b/owners.php new file mode 100644 index 0000000..d11d4f3 --- /dev/null +++ b/owners.php @@ -0,0 +1,6 @@ + + +

Manage Owners

+

This section is under construction.

+ + diff --git a/properties.php b/properties.php new file mode 100644 index 0000000..676e78f --- /dev/null +++ b/properties.php @@ -0,0 +1,6 @@ + + +

Manage Properties

+

This section is under construction.

+ + diff --git a/transactions.php b/transactions.php new file mode 100644 index 0000000..600f7fa --- /dev/null +++ b/transactions.php @@ -0,0 +1,6 @@ + + +

Manage Transactions

+

This section is under construction.

+ +