fields as $field) { $values[$field->code] = self::evaluateSingleField($field, $record, $values); } return $values; } protected static function evaluateSingleField($field, Model $record, array $evaluatedValues): mixed { $config = $field->source_config ?? []; return match ($field->source_type) { 'db_column' => self::getDbColumnValue($record, $config['column'] ?? null), 'db_relation' => self::getRelationValue($record, $config), 'formula' => self::evaluateFormula($config['expression'] ?? '', $evaluatedValues), 'input' => $config['default'] ?? '', 'static' => $config['value'] ?? '', default => '', }; } protected static function getDbColumnValue(Model $record, ?string $column): mixed { if (! $column) return ''; return $record->{$column} ?? ''; } protected static function getRelationValue(Model $record, array $config): mixed { $relation = $config['relation'] ?? null; $column = $config['column'] ?? null; $index = $config['index'] ?? null; if (! $relation || ! $column) return ''; $related = $record->{$relation}; if (is_null($related)) return ''; if ($related instanceof \Illuminate\Database\Eloquent\Collection) { if ($index !== null) { $item = $related->skip($index)->first(); return $item?->{$column} ?? ''; } return $related->pluck($column)->implode(', '); } return $related->{$column} ?? ''; } protected static function evaluateFormula(string $expression, array $values): float { if (empty($expression)) return 0; // Thay thế tên biến bằng giá trị $evalExpression = $expression; foreach ($values as $key => $value) { if (is_numeric($value)) { $evalExpression = str_replace($key, $value, $evalExpression); } } // Chỉ cho phép số, dấu chấm, dấu phẩy và các phép toán cơ bản $evalExpression = str_replace(',', '.', $evalExpression); $evalExpression = preg_replace('/[^0-9.\+\-\*\/\(\)\s]/', '', $evalExpression); $evalExpression = str_replace(' ', '', $evalExpression); if (empty($evalExpression)) return 0; try { $result = self::safeCalculate($evalExpression); return (float) $result; } catch (\Throwable $e) { return 0; } } protected static function safeCalculate(string $expression): float { // Tokenize: tách số và operators $tokens = []; $number = ''; for ($i = 0; $i < strlen($expression); $i++) { $char = $expression[$i]; if (ctype_digit($char) || $char === '.') { $number .= $char; } else { if ($number !== '') { $tokens[] = (float) $number; $number = ''; } $tokens[] = $char; } } if ($number !== '') { $tokens[] = (float) $number; } // Shunting yard algorithm: infix → postfix $output = []; $stack = []; $precedence = ['+' => 1, '-' => 1, '*' => 2, '/' => 2]; foreach ($tokens as $token) { if (is_numeric($token)) { $output[] = $token; } elseif ($token === '(') { $stack[] = $token; } elseif ($token === ')') { while (!empty($stack) && end($stack) !== '(') { $output[] = array_pop($stack); } array_pop($stack); // pop '(' } else { // Operator while (!empty($stack) && end($stack) !== '(' && isset($precedence[end($stack)]) && $precedence[end($stack)] >= $precedence[$token]) { $output[] = array_pop($stack); } $stack[] = $token; } } while (!empty($stack)) { $output[] = array_pop($stack); } // Evaluate postfix $evalStack = []; foreach ($output as $token) { if (is_numeric($token)) { $evalStack[] = $token; } else { $b = array_pop($evalStack); $a = array_pop($evalStack); if ($a === null || $b === null) { throw new \InvalidArgumentException('Invalid expression'); } switch ($token) { case '+': $evalStack[] = bcadd((string) $a, (string) $b, 10); break; case '-': $evalStack[] = bcsub((string) $a, (string) $b, 10); break; case '*': $evalStack[] = bcmul((string) $a, (string) $b, 10); break; case '/': if ((float) $b == 0) throw new \InvalidArgumentException('Division by zero'); $evalStack[] = bcdiv((string) $a, (string) $b, 10); break; } } } if (count($evalStack) !== 1) { throw new \InvalidArgumentException('Invalid expression'); } return (float) $evalStack[0]; } /** * Format value theo kiểu field. */ public static function formatValue(mixed $value, string $format, int $decimals = 0): string { return match ($format) { 'number' => number_format((float) $value, $decimals, ',', '.'), 'currency' => number_format((float) $value, 0, ',', '.') . ' VNĐ', 'percent' => number_format((float) $value, $decimals, ',', '.') . '%', 'date' => $value ? \Carbon\Carbon::parse($value)->format('d/m/Y') : '', default => (string) $value, }; } /** * Render template with evaluated values. */ public static function render(FormTemplate $template, Model $record): array { $rawValues = self::evaluateFields($template, $record); $formattedValues = []; foreach ($template->fields as $field) { $code = $field->code; $rawValue = $rawValues[$code] ?? ''; $formattedValues[$code] = self::formatValue( $rawValue, $field->format, $field->decimal_places ); } $html = $template->html_template; foreach ($formattedValues as $code => $value) { $html = str_replace('{{' . $code . '}}', (string) $value, $html); } return [ 'html' => $html, 'raw_values' => $rawValues, 'formatted_values' => $formattedValues, ]; } /** * Save print log with snapshot. */ public static function savePrintLog(FormTemplate $template, Model $record, array $renderResult, int $userId): FormPrintLog { return FormPrintLog::create([ 'template_id' => $template->id, 'target_model' => get_class($record), 'target_id' => $record->id, 'target_number' => $record->contract_number ?? $record->code ?? null, 'snapshot_data' => [ 'raw_values' => $renderResult['raw_values'], 'formatted_values' => $renderResult['formatted_values'], ], 'rendered_html' => $renderResult['html'], 'printed_by' => $userId, 'printed_at' => now(), ]); } }