Commit c8b79155 authored by Administrator's avatar Administrator

Update 15 files via Son of Anton

parent 216ae487
<?php
declare(strict_types=1);
namespace App\Modules\Workflow\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Workflow\Models\WorkflowDefinition;
use App\Modules\Workflow\Models\WorkflowInstance;
use App\Modules\Workflow\Models\WorkflowTransitionLog;
use App\Modules\Workflow\Services\WorkflowEngine;
class WorkflowController extends Controller
{
public function index(Request $request): Response
{
$definitions = WorkflowDefinition::allActive();
$db = App::getInstance()->db();
$stats = [];
foreach ($definitions as $def) {
$activeCount = $db->selectOne(
"SELECT COUNT(*) as cnt FROM workflow_instances WHERE workflow_code = ? AND is_completed = 0",
[$def['workflow_code']]
);
$completedCount = $db->selectOne(
"SELECT COUNT(*) as cnt FROM workflow_instances WHERE workflow_code = ? AND is_completed = 1",
[$def['workflow_code']]
);
$stats[$def['workflow_code']] = [
'active' => (int) ($activeCount['cnt'] ?? 0),
'completed' => (int) ($completedCount['cnt'] ?? 0),
];
}
return $this->view('Workflow.Views.index', [
'definitions' => $definitions,
'stats' => $stats,
]);
}
public function instance(Request $request, string $id): Response
{
$instance = WorkflowInstance::find((int) $id);
if (!$instance) {
return $this->redirect('/workflows')->withError('مثيل دورة العمل غير موجود');
}
$definition = $instance->getDefinition();
$history = WorkflowEngine::getHistory((int) $instance->id);
$available = WorkflowEngine::getAvailableTransitions((int) $instance->id);
return $this->view('Workflow.Views.instance', [
'instance' => $instance,
'definition' => $definition,
'history' => $history,
'available' => $available,
]);
}
public function transition(Request $request, string $id): Response
{
$transitionName = trim((string) $request->post('transition_name', ''));
$notes = trim((string) $request->post('notes', ''));
if ($transitionName === '') {
return $this->redirect("/workflows/instances/{$id}")->withError('يرجى تحديد الانتقال');
}
try {
WorkflowEngine::transition((int) $id, $transitionName, $notes ?: null, 'manual');
return $this->redirect("/workflows/instances/{$id}")->withSuccess('تم تنفيذ الانتقال بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect("/workflows/instances/{$id}")->withError('فشل تنفيذ الانتقال: ' . $e->getMessage());
}
}
public function diagram(Request $request, string $code): Response
{
$definition = WorkflowDefinition::findByCode($code);
if (!$definition) {
return $this->redirect('/workflows')->withError('تعريف دورة العمل غير موجود');
}
return $this->view('Workflow.Views.diagram', [
'definition' => $definition,
]);
}
public function instances(Request $request, string $code): Response
{
$db = App::getInstance()->db();
$status = $request->get('status', '');
$where = "wi.workflow_code = ?";
$params = [$code];
if ($status === 'active') {
$where .= " AND wi.is_completed = 0";
} elseif ($status === 'completed') {
$where .= " AND wi.is_completed = 1";
}
$rows = $db->select(
"SELECT wi.*, wd.name_ar as definition_name_ar
FROM workflow_instances wi
JOIN workflow_definitions wd ON wd.id = wi.workflow_definition_id
WHERE {$where}
ORDER BY wi.updated_at DESC
LIMIT 100",
$params
);
$definition = WorkflowDefinition::findByCode($code);
return $this->view('Workflow.Views.index', [
'definitions' => WorkflowDefinition::allActive(),
'stats' => [],
'filteredInstances' => $rows,
'filterCode' => $code,
'filterStatus' => $status,
'currentDef' => $definition,
]);
}
public function processTimeouts(Request $request): Response
{
$processed = WorkflowEngine::processTimeouts();
$count = count($processed);
return $this->redirect('/workflows')->withSuccess("تم معالجة {$count} انتقال تلقائي");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Workflow\Models;
use App\Core\Model;
use App\Core\App;
class WorkflowDefinition extends Model
{
protected static string $table = 'workflow_definitions';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'workflow_code', 'name_ar', 'name_en', 'description_ar',
'definition_json', 'version', 'is_active',
];
public function getDefinition(): array
{
return json_decode($this->definition_json ?? '{"states":{},"transitions":[]}', true) ?? ['states' => [], 'transitions' => []];
}
public function getStates(): array
{
$def = $this->getDefinition();
return $def['states'] ?? [];
}
public function getTransitions(): array
{
$def = $this->getDefinition();
return $def['transitions'] ?? [];
}
public function getTransitionsFrom(string $state): array
{
$transitions = $this->getTransitions();
return array_filter($transitions, fn($t) => ($t['from'] ?? '') === $state);
}
public function getInitialState(): ?string
{
foreach ($this->getStates() as $key => $state) {
if (($state['type'] ?? '') === 'initial') {
return $key;
}
}
$states = $this->getStates();
return !empty($states) ? array_key_first($states) : null;
}
public function findTransition(string $name): ?array
{
foreach ($this->getTransitions() as $t) {
if (($t['name'] ?? '') === $name) {
return $t;
}
}
return null;
}
public static function findByCode(string $code): ?static
{
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT * FROM workflow_definitions WHERE workflow_code = ? AND is_active = 1", [$code]);
if (!$row) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function allActive(): array
{
$db = App::getInstance()->db();
return $db->select("SELECT * FROM workflow_definitions WHERE is_active = 1 ORDER BY name_ar");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Workflow\Models;
use App\Core\Model;
use App\Core\App;
class WorkflowInstance extends Model
{
protected static string $table = 'workflow_instances';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'workflow_definition_id', 'workflow_code', 'entity_type', 'entity_id',
'current_state', 'state_entered_at', 'state_data_json',
'is_completed', 'completed_at',
];
public function getStateData(): array
{
return json_decode($this->state_data_json ?? '{}', true) ?? [];
}
public function getDefinition(): ?WorkflowDefinition
{
return WorkflowDefinition::find((int) $this->workflow_definition_id);
}
public static function findForEntity(string $entityType, int $entityId, ?string $workflowCode = null): ?static
{
$db = App::getInstance()->db();
$sql = "SELECT * FROM workflow_instances WHERE entity_type = ? AND entity_id = ? AND is_completed = 0";
$params = [$entityType, $entityId];
if ($workflowCode !== null) {
$sql .= " AND workflow_code = ?";
$params[] = $workflowCode;
}
$sql .= " ORDER BY created_at DESC LIMIT 1";
$row = $db->selectOne($sql, $params);
if (!$row) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function allForEntity(string $entityType, int $entityId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT wi.*, wd.name_ar as definition_name_ar FROM workflow_instances wi JOIN workflow_definitions wd ON wd.id = wi.workflow_definition_id WHERE wi.entity_type = ? AND wi.entity_id = ? ORDER BY wi.created_at DESC",
[$entityType, $entityId]
);
}
public static function getActiveByState(string $workflowCode, string $state): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM workflow_instances WHERE workflow_code = ? AND current_state = ? AND is_completed = 0",
[$workflowCode, $state]
);
}
public static function getTimedOut(string $workflowCode, string $state, int $timeoutDays): array
{
$db = App::getInstance()->db();
$cutoff = date('Y-m-d H:i:s', time() - ($timeoutDays * 86400));
return $db->select(
"SELECT * FROM workflow_instances WHERE workflow_code = ? AND current_state = ? AND is_completed = 0 AND state_entered_at <= ?",
[$workflowCode, $state, $cutoff]
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Workflow\Models;
use App\Core\App;
class WorkflowTransitionLog
{
public static function create(array $data): int
{
$db = App::getInstance()->db();
return $db->insert('workflow_transition_log', $data);
}
public static function getForInstance(int $instanceId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT wtl.*, e.full_name_ar as employee_name FROM workflow_transition_log wtl LEFT JOIN employees e ON e.id = wtl.triggered_by_employee_id WHERE wtl.workflow_instance_id = ? ORDER BY wtl.created_at ASC",
[$instanceId]
);
}
public static function getLatest(int $instanceId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT * FROM workflow_transition_log WHERE workflow_instance_id = ? ORDER BY created_at DESC LIMIT 1",
[$instanceId]
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/workflows', 'Workflow\Controllers\WorkflowController@index', ['auth', 'csrf'], 'rules.view'],
['GET', '/workflows/diagram/{code}', 'Workflow\Controllers\WorkflowController@diagram', ['auth', 'csrf'], 'rules.view'],
['GET', '/workflows/instances/{code}', 'Workflow\Controllers\WorkflowController@instances', ['auth', 'csrf'], 'rules.view'],
['GET', '/workflows/instances/{id:\d+}', 'Workflow\Controllers\WorkflowController@instance', ['auth', 'csrf'], 'rules.view'],
['POST', '/workflows/instances/{id:\d+}/transition','Workflow\Controllers\WorkflowController@transition', ['auth', 'csrf'], 'rules.edit'],
['POST', '/workflows/process-timeouts', 'Workflow\Controllers\WorkflowController@processTimeouts', ['auth', 'csrf'], 'rules.edit'],
];
\ No newline at end of file
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\Workflow\Services;
use App\Core\App;
use App\Core\Logger;
use App\Core\Registries\ActionRegistry;
use App\Modules\Workflow\Models\WorkflowInstance;
final class WorkflowGuardEvaluator
{
/**
* Evaluate a single guard condition against a workflow instance.
*/
public static function evaluate(array $guard, WorkflowInstance $instance): bool
{
$type = $guard['type'] ?? '';
$value = $guard['value'] ?? '';
return match ($type) {
'permission' => self::evaluatePermission($value),
'role' => self::evaluateRole($value),
'condition' => self::evaluateCondition($value, $instance),
'timeout' => self::evaluateTimeout($guard, $instance),
default => self::evaluateCustom($type, $value, $instance),
};
}
/**
* Check if current employee has the required permission.
*/
private static function evaluatePermission(string $permissionKey): bool
{
$employee = App::getInstance()->currentEmployee();
if (!$employee) {
return false;
}
if (method_exists($employee, 'hasPermission')) {
return $employee->hasPermission($permissionKey);
}
return false;
}
/**
* Check if current employee has the required role.
*/
private static function evaluateRole(string $roleCode): bool
{
$employee = App::getInstance()->currentEmployee();
if (!$employee) {
return false;
}
if (method_exists($employee, 'hasRole')) {
return $employee->hasRole($roleCode);
}
return false;
}
/**
* Evaluate a named condition.
* Conditions can be registered by other modules via ActionRegistry.
*/
private static function evaluateCondition(string $conditionName, WorkflowInstance $instance): bool
{
// Check if a condition handler is registered
$handler = ActionRegistry::get('guard_condition.' . $conditionName);
if ($handler !== null && isset($handler['handler']) && is_callable($handler['handler'])) {
return (bool) ($handler['handler'])([
'instance' => $instance,
'entity_type' => $instance->entity_type,
'entity_id' => (int) $instance->entity_id,
'state_data' => $instance->getStateData(),
]);
}
// Built-in conditions
return match ($conditionName) {
'has_payment_receipt' => self::checkHasPaymentReceipt($instance),
'is_subscription_paid' => self::checkSubscriptionPaid($instance),
'within_appeal_window' => self::checkWithinAppealWindow($instance),
'five_years_unpaid' => self::checkFiveYearsUnpaid($instance),
default => self::evaluateFallbackCondition($conditionName, $instance),
};
}
/**
* Evaluate timeout guard — has enough time passed since entering current state?
*/
private static function evaluateTimeout(array $guard, WorkflowInstance $instance): bool
{
$days = (int) ($guard['days'] ?? 0);
if ($days <= 0) {
return false;
}
$enteredAt = strtotime($instance->state_entered_at);
if ($enteredAt === false) {
return false;
}
$timeoutAt = $enteredAt + ($days * 86400);
return time() >= $timeoutAt;
}
/**
* Custom guard type — check ActionRegistry for a handler.
*/
private static function evaluateCustom(string $type, string $value, WorkflowInstance $instance): bool
{
$handler = ActionRegistry::get('guard_type.' . $type);
if ($handler !== null && isset($handler['handler']) && is_callable($handler['handler'])) {
return (bool) ($handler['handler'])([
'value' => $value,
'instance' => $instance,
'entity_type' => $instance->entity_type,
'entity_id' => (int) $instance->entity_id,
]);
}
Logger::warning("Unknown guard type with no registered handler: {$type}", [
'value' => $value,
'instance_id' => (int) $instance->id,
]);
return true; // Unknown guards pass by default to not block workflows
}
// ── Built-in condition implementations ──
private static function checkHasPaymentReceipt(WorkflowInstance $instance): bool
{
$db = App::getInstance()->db();
// Check if payments table exists
if (!$db->tableExists('payments')) {
// Phase 11 not yet built — check state_data for manual receipt
$stateData = $instance->getStateData();
return !empty($stateData['receipt_number']);
}
$payment = $db->selectOne(
"SELECT id FROM payments WHERE related_entity_type = ? AND related_entity_id = ? AND is_voided = 0 LIMIT 1",
[$instance->entity_type, (int) $instance->entity_id]
);
return $payment !== null;
}
private static function checkSubscriptionPaid(WorkflowInstance $instance): bool
{
$db = App::getInstance()->db();
if (!$db->tableExists('subscriptions')) {
return true; // Phase 12 not yet built
}
$currentYear = date('Y');
$unpaid = $db->selectOne(
"SELECT id FROM subscriptions WHERE member_id = ? AND financial_year = ? AND status NOT IN ('paid', 'exempt') LIMIT 1",
[(int) $instance->entity_id, $currentYear]
);
return $unpaid === null;
}
private static function checkWithinAppealWindow(WorkflowInstance $instance): bool
{
$enteredAt = strtotime($instance->state_entered_at);
if ($enteredAt === false) {
return false;
}
// Default 15 days appeal window — can be overridden by rules engine
$appealDays = 15;
try {
$db = App::getInstance()->db();
if ($db->tableExists('business_rules')) {
$rule = $db->selectOne(
"SELECT current_value_json FROM business_rules WHERE rule_code = 'APPEAL_WINDOW' AND is_active = 1 AND branch_id IS NULL",
[]
);
if ($rule) {
$val = json_decode($rule['current_value_json'], true);
$appealDays = (int) ($val['days'] ?? 15);
}
}
} catch (\Throwable $e) {
// Use default
}
$deadline = $enteredAt + ($appealDays * 86400);
return time() <= $deadline;
}
private static function checkFiveYearsUnpaid(WorkflowInstance $instance): bool
{
$db = App::getInstance()->db();
if (!$db->tableExists('subscriptions')) {
return false; // Phase 12 not yet built
}
$currentYear = (int) date('Y');
$unpaidYears = 0;
for ($y = $currentYear; $y >= $currentYear - 6; $y--) {
$unpaid = $db->selectOne(
"SELECT id FROM subscriptions WHERE member_id = ? AND financial_year = ? AND status NOT IN ('paid', 'exempt') LIMIT 1",
[(int) $instance->entity_id, (string) $y]
);
if ($unpaid) {
$unpaidYears++;
} else {
break;
}
}
return $unpaidYears >= 5;
}
private static function evaluateFallbackCondition(string $conditionName, WorkflowInstance $instance): bool
{
// Check state_data for manual condition flags
$stateData = $instance->getStateData();
if (isset($stateData[$conditionName])) {
return (bool) $stateData[$conditionName];
}
Logger::warning("Unknown condition with no handler and no state_data flag: {$conditionName}", [
'instance_id' => (int) $instance->id,
]);
return false;
}
}
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مخطط: <?= e($definition->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/workflows" class="btn btn-outline">← العودة</a>
<a href="/workflows/instances/<?= e($definition->workflow_code) ?>" class="btn btn-outline">عرض المثيلات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$states = $definition->getStates();
$transitions = $definition->getTransitions();
?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<div>
<h2 style="margin:0;color:#0D7377;"><?= e($definition->name_ar) ?></h2>
<p style="color:#6B7280;margin:5px 0 0;"><?= e($definition->description_ar ?? '') ?></p>
</div>
<div style="text-align:left;">
<code style="font-size:12px;color:#9CA3AF;"><?= e($definition->workflow_code) ?></code><br>
<small style="color:#6B7280;">الإصدار <?= (int) $definition->version ?></small>
</div>
</div>
<!-- State Diagram (CSS-based flowchart) -->
<div style="display:flex;flex-wrap:wrap;gap:20px;justify-content:center;padding:30px 0;">
<?php foreach ($states as $key => $state): ?>
<?php
$type = $state['type'] ?? 'intermediate';
$bgColor = match($type) {
'initial' => '#EFF6FF',
'terminal' => '#FEF2F2',
'intermediate' => '#FFFFFF',
default => '#F9FAFB',
};
$borderColor = match($type) {
'initial' => '#0284C7',
'terminal' => '#DC2626',
'intermediate' => '#0D7377',
default => '#E5E7EB',
};
$shape = match($type) {
'initial' => 'border-radius:50%;width:120px;height:120px;',
'terminal' => 'border-radius:8px;border-width:3px;',
default => 'border-radius:8px;',
};
?>
<div id="state-<?= e($key) ?>" style="background:<?= $bgColor ?>;border:2px solid <?= $borderColor ?>;padding:15px 20px;text-align:center;min-width:140px;<?= $shape ?>display:flex;flex-direction:column;align-items:center;justify-content:center;">
<div style="font-weight:700;color:#1A1A2E;font-size:14px;"><?= e($state['label_ar'] ?? $key) ?></div>
<div style="font-size:10px;color:#9CA3AF;margin-top:4px;"><?= e($key) ?></div>
<?php if ($type === 'initial'): ?>
<div style="font-size:10px;color:#0284C7;margin-top:2px;">▶ بداية</div>
<?php elseif ($type === 'terminal'): ?>
<div style="font-size:10px;color:#DC2626;margin-top:2px;">■ نهاية</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Transition Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">جدول الانتقالات</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>اسم الانتقال</th>
<th>من</th>
<th>إلى</th>
<th>النوع</th>
<th>الشروط</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($transitions as $t): ?>
<?php
$fromState = $states[$t['from']] ?? [];
$toState = $states[$t['to']] ?? [];
$trigger = $t['trigger'] ?? 'manual';
?>
<tr>
<td style="font-weight:600;"><?= e($t['name']) ?></td>
<td>
<span style="color:#DC2626;font-size:13px;"><?= e($fromState['label_ar'] ?? $t['from']) ?></span>
</td>
<td>
<span style="color:#059669;font-size:13px;"><?= e($toState['label_ar'] ?? $t['to']) ?></span>
</td>
<td>
<span style="background:<?= $trigger === 'automatic' ? '#FFF7ED' : '#F0FDF4' ?>;color:<?= $trigger === 'automatic' ? '#D97706' : '#059669' ?>;padding:2px 8px;border-radius:4px;font-size:12px;">
<?= $trigger === 'automatic' ? 'تلقائي' : 'يدوي' ?>
</span>
</td>
<td style="font-size:12px;">
<?php $guards = $t['guards'] ?? []; ?>
<?php if (empty($guards)): ?>
<span style="color:#9CA3AF;">بدون شروط</span>
<?php else: ?>
<?php foreach ($guards as $g): ?>
<div style="margin-bottom:2px;">
<code style="font-size:11px;background:#F3F4F6;padding:1px 4px;border-radius:2px;"><?= e($g['type'] ?? '') ?></code>
<span style="color:#6B7280;"><?= e(is_array($g['value'] ?? '') ? json_encode($g['value']) : (string)($g['value'] ?? '')) ?></span>
<?php if (isset($g['days'])): ?><span style="color:#D97706;"><?= (int) $g['days'] ?> يوم</span><?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</td>
<td style="font-size:12px;">
<?php $actions = $t['actions'] ?? []; ?>
<?php if (empty($actions)): ?>
<span style="color:#9CA3AF;"></span>
<?php else: ?>
<?php foreach ($actions as $a): ?>
<div><code style="font-size:11px;"><?= e($a['type'] ?? '') ?>: <?= e($a['value'] ?? '') ?></code></div>
<?php endforeach; ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- States Summary -->
<div class="card" style="margin-top:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">ملخص الحالات (<?= count($states) ?>)</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:10px;">
<?php foreach ($states as $key => $state): ?>
<?php
$type = $state['type'] ?? 'intermediate';
$color = match($type) {
'initial' => '#0284C7',
'terminal' => '#DC2626',
default => '#0D7377',
};
?>
<div style="padding:10px 15px;border:1px solid #E5E7EB;border-right:4px solid <?= $color ?>;border-radius:6px;">
<div style="font-weight:600;color:#1A1A2E;"><?= e($state['label_ar'] ?? $key) ?></div>
<div style="font-size:11px;color:#9CA3AF;"><?= e($key) ?> · <?= e($type) ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>محرك دورات العمل<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<form method="POST" action="/workflows/process-timeouts" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" onclick="return confirm('هل تريد معالجة الانتقالات التلقائية المنتهية؟')">⏱ معالجة المهلات المنتهية</button>
</form>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Workflow Definitions Overview -->
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(300px, 1fr));gap:20px;margin-bottom:30px;">
<?php foreach ($definitions as $def): ?>
<?php $s = $stats[$def['workflow_code']] ?? ['active' => 0, 'completed' => 0]; ?>
<div class="card" style="padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:15px;">
<div>
<h3 style="margin:0;color:#1A1A2E;"><?= e($def['name_ar']) ?></h3>
<code style="font-size:11px;color:#9CA3AF;"><?= e($def['workflow_code']) ?></code>
</div>
<span style="background:#F3F4F6;padding:2px 8px;border-radius:4px;font-size:12px;">v<?= (int) $def['version'] ?></span>
</div>
<p style="color:#6B7280;font-size:13px;margin-bottom:15px;"><?= e($def['description_ar'] ?? '') ?></p>
<div style="display:flex;gap:15px;margin-bottom:15px;">
<div style="text-align:center;">
<div style="font-size:24px;font-weight:700;color:#0D7377;"><?= $s['active'] ?></div>
<div style="font-size:11px;color:#6B7280;">نشط</div>
</div>
<div style="text-align:center;">
<div style="font-size:24px;font-weight:700;color:#059669;"><?= $s['completed'] ?></div>
<div style="font-size:11px;color:#6B7280;">مكتمل</div>
</div>
</div>
<div style="display:flex;gap:8px;">
<a href="/workflows/diagram/<?= e($def['workflow_code']) ?>" class="btn btn-sm btn-outline">المخطط</a>
<a href="/workflows/instances/<?= e($def['workflow_code']) ?>" class="btn btn-sm btn-outline">المثيلات</a>
<a href="/workflows/instances/<?= e($def['workflow_code']) ?>?status=active" class="btn btn-sm btn-outline" style="color:#D97706;">النشطة</a>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Filtered Instances (if viewing specific workflow) -->
<?php if (!empty($filteredInstances)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;color:#0D7377;">
مثيلات: <?= e($currentDef ? $currentDef->name_ar : ($filterCode ?? '')) ?>
<?php if (!empty($filterStatus)): ?>
<span style="font-size:14px;color:#6B7280;">(<?= $filterStatus === 'active' ? 'نشطة' : ($filterStatus === 'completed' ? 'مكتملة' : 'الكل') ?>)</span>
<?php endif; ?>
</h3>
<a href="/workflows" class="btn btn-sm btn-outline">← العودة</a>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>نوع الكيان</th>
<th>رقم الكيان</th>
<th>الحالة الحالية</th>
<th>دخول الحالة</th>
<th>مكتمل</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($filteredInstances as $inst): ?>
<?php
$stateLabel = $inst['current_state'];
if ($currentDef) {
$states = $currentDef->getStates();
$stateLabel = $states[$inst['current_state']]['label_ar'] ?? $inst['current_state'];
}
?>
<tr>
<td><?= (int) $inst['id'] ?></td>
<td><code style="font-size:12px;"><?= e($inst['entity_type']) ?></code></td>
<td><?= (int) $inst['entity_id'] ?></td>
<td>
<span style="background:<?= $inst['is_completed'] ? '#F0FDF4' : '#FFF7ED' ?>;color:<?= $inst['is_completed'] ? '#059669' : '#D97706' ?>;padding:3px 10px;border-radius:4px;font-size:13px;font-weight:600;">
<?= e($stateLabel) ?>
</span>
</td>
<td style="font-size:12px;"><?= e($inst['state_entered_at']) ?></td>
<td><?= $inst['is_completed'] ? '<span style="color:#059669;">✓</span>' : '<span style="color:#D97706;">—</span>' ?></td>
<td>
<a href="/workflows/instances/<?= (int) $inst['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($filteredInstances)): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#6B7280;">لا توجد مثيلات</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مثيل دورة العمل #<?= (int) $instance->id ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/workflows" class="btn btn-outline">← العودة</a>
<a href="/workflows/diagram/<?= e($instance->workflow_code) ?>" class="btn btn-outline">عرض المخطط</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$states = $definition ? $definition->getStates() : [];
$currentStateInfo = $states[$instance->current_state] ?? [];
?>
<div style="display:grid;grid-template-columns:1fr 350px;gap:20px;">
<div>
<!-- Instance Info -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">معلومات المثيل</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:35%;">دورة العمل</td><td style="padding:6px 0;font-weight:600;"><?= e($definition ? $definition->name_ar : $instance->workflow_code) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نوع الكيان</td><td><code><?= e($instance->entity_type) ?></code></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم الكيان</td><td><?= (int) $instance->entity_id ?></td></tr>
<tr>
<td style="padding:6px 0;color:#6B7280;">الحالة الحالية</td>
<td>
<span style="background:<?= $instance->is_completed ? '#F0FDF4' : '#EFF6FF' ?>;color:<?= $instance->is_completed ? '#059669' : '#0284C7' ?>;padding:4px 12px;border-radius:6px;font-weight:700;font-size:15px;">
<?= e($currentStateInfo['label_ar'] ?? $instance->current_state) ?>
</span>
</td>
</tr>
<tr><td style="padding:6px 0;color:#6B7280;">دخول الحالة</td><td><?= e($instance->state_entered_at) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الإنشاء</td><td><?= e($instance->created_at) ?></td></tr>
<tr>
<td style="padding:6px 0;color:#6B7280;">مكتمل</td>
<td>
<?php if ($instance->is_completed): ?>
<span style="color:#059669;font-weight:600;">✓ نعم — <?= e($instance->completed_at) ?></span>
<?php else: ?>
<span style="color:#D97706;">جارية</span>
<?php endif; ?>
</td>
</tr>
</table>
</div>
</div>
<!-- Transition History -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">سجل الانتقالات</h3>
</div>
<div style="padding:20px;">
<?php if (empty($history)): ?>
<div style="text-align:center;padding:30px;color:#6B7280;">لا توجد انتقالات مسجلة</div>
<?php else: ?>
<?php foreach ($history as $i => $h): ?>
<?php
$fromInfo = $states[$h['from_state']] ?? [];
$toInfo = $states[$h['to_state']] ?? [];
?>
<div style="display:flex;gap:15px;padding:15px 0;<?= $i < count($history) - 1 ? 'border-bottom:1px solid #F3F4F6;' : '' ?>">
<div style="flex-shrink:0;width:10px;position:relative;">
<div style="width:10px;height:10px;border-radius:50%;background:#0D7377;margin-top:5px;"></div>
<?php if ($i < count($history) - 1): ?>
<div style="position:absolute;top:15px;right:4px;width:2px;height:calc(100% + 15px);background:#E5E7EB;"></div>
<?php endif; ?>
</div>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;margin-bottom:5px;">
<strong style="color:#1A1A2E;"><?= e($h['transition_name']) ?></strong>
<span style="color:#9CA3AF;font-size:12px;"><?= e($h['created_at']) ?></span>
</div>
<div style="font-size:13px;color:#6B7280;margin-bottom:4px;">
<span style="color:#DC2626;"><?= e($fromInfo['label_ar'] ?? $h['from_state']) ?></span>
<span style="color:#059669;"><?= e($toInfo['label_ar'] ?? $h['to_state']) ?></span>
</div>
<div style="font-size:12px;color:#9CA3AF;">
<?= e($h['employee_name'] ?? 'النظام') ?>
· <?= e($h['trigger_type']) ?>
</div>
<?php if ($h['notes']): ?>
<div style="font-size:13px;color:#4B5563;margin-top:4px;background:#F9FAFB;padding:6px 10px;border-radius:4px;"><?= e($h['notes']) ?></div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<!-- Available Transitions -->
<div>
<?php if (!$instance->is_completed && !empty($available)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<h4 style="margin:0;color:#1A1A2E;font-size:14px;">الانتقالات المتاحة</h4>
</div>
<div style="padding:15px;">
<?php foreach ($available as $trans): ?>
<form method="POST" action="/workflows/instances/<?= (int) $instance->id ?>/transition" style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #F3F4F6;">
<?= csrf_field() ?>
<input type="hidden" name="transition_name" value="<?= e($trans['name']) ?>">
<div style="margin-bottom:8px;">
<strong style="color:#1A1A2E;font-size:14px;"><?= e($trans['name']) ?></strong>
<div style="font-size:12px;color:#6B7280;"><?= e($trans['to_label_ar']) ?></div>
</div>
<div class="form-group" style="margin-bottom:8px;">
<textarea name="notes" class="form-textarea" rows="2" placeholder="ملاحظات (اختياري)" style="font-size:12px;"></textarea>
</div>
<?php if ($trans['can_execute']): ?>
<button type="submit" class="btn btn-sm btn-primary" style="width:100%;" onclick="return confirm('هل تريد تنفيذ هذا الانتقال؟')">تنفيذ</button>
<?php else: ?>
<button type="button" class="btn btn-sm" style="width:100%;background:#E5E7EB;color:#9CA3AF;cursor:not-allowed;" disabled>غير متاح (شرط غير مستوفى)</button>
<?php endif; ?>
</form>
<?php endforeach; ?>
</div>
</div>
<?php elseif ($instance->is_completed): ?>
<div class="card" style="padding:30px;text-align:center;">
<div style="font-size:48px;margin-bottom:10px;"></div>
<strong style="color:#059669;">دورة العمل مكتملة</strong>
</div>
<?php else: ?>
<div class="card" style="padding:30px;text-align:center;color:#6B7280;">
لا توجد انتقالات يدوية متاحة
</div>
<?php endif; ?>
<!-- State Map -->
<div class="card">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<h4 style="margin:0;color:#1A1A2E;font-size:14px;">خريطة الحالات</h4>
</div>
<div style="padding:15px;">
<?php foreach ($states as $key => $state): ?>
<div style="padding:6px 10px;margin-bottom:4px;border-radius:4px;font-size:13px;<?= $key === $instance->current_state ? 'background:#EFF6FF;border:2px solid #0284C7;font-weight:700;' : 'background:#F9FAFB;border:1px solid #E5E7EB;' ?>">
<?php if ($key === $instance->current_state): ?><?php elseif (($state['type'] ?? '') === 'terminal'): ?><?php else: ?><?php endif; ?>
<?= e($state['label_ar'] ?? $key) ?>
<?php if (($state['type'] ?? '') === 'initial'): ?><small style="color:#059669;">(بداية)</small><?php endif; ?>
<?php if (($state['type'] ?? '') === 'terminal'): ?><small style="color:#DC2626;">(نهاية)</small><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
MenuRegistry::register('workflows', [
'label_ar' => 'دورات العمل',
'label_en' => 'Workflows',
'icon' => '🔄',
'route' => '/workflows',
'permission' => 'rules.view',
'order' => 165,
'children' => [
['label_ar' => 'نظرة عامة', 'label_en' => 'Overview', 'route' => '/workflows', 'permission' => 'rules.view', 'order' => 1],
],
]);
PermissionRegistry::register('workflows', [
'workflow.view' => ['ar' => 'عرض دورات العمل', 'en' => 'View Workflows'],
'workflow.transition' => ['ar' => 'تنفيذ الانتقالات', 'en' => 'Execute Transitions'],
'workflow.manage' => ['ar' => 'إدارة دورات العمل', 'en' => 'Manage Workflows'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `workflow_definitions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`workflow_code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`description_ar` TEXT NULL,
`definition_json` JSON NOT NULL,
`version` INT UNSIGNED NOT NULL DEFAULT 1,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_workflow_defs_code` (`workflow_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `workflow_definitions`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `workflow_instances` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`workflow_definition_id` BIGINT UNSIGNED NOT NULL,
`workflow_code` VARCHAR(50) NOT NULL,
`entity_type` VARCHAR(100) NOT NULL,
`entity_id` BIGINT UNSIGNED NOT NULL,
`current_state` VARCHAR(50) NOT NULL,
`state_entered_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`state_data_json` JSON NULL,
`is_completed` TINYINT(1) NOT NULL DEFAULT 0,
`completed_at` TIMESTAMP NULL DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_workflow_inst_def` (`workflow_definition_id`),
INDEX `idx_workflow_inst_entity` (`entity_type`, `entity_id`),
INDEX `idx_workflow_inst_state` (`current_state`),
INDEX `idx_workflow_inst_completed` (`is_completed`),
CONSTRAINT `fk_workflow_inst_def` FOREIGN KEY (`workflow_definition_id`) REFERENCES `workflow_definitions`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `workflow_instances`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `workflow_transition_log` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`workflow_instance_id` BIGINT UNSIGNED NOT NULL,
`from_state` VARCHAR(50) NOT NULL,
`to_state` VARCHAR(50) NOT NULL,
`transition_name` VARCHAR(100) NOT NULL,
`triggered_by_employee_id` BIGINT UNSIGNED NULL,
`trigger_type` VARCHAR(30) NOT NULL DEFAULT 'manual',
`guard_results_json` JSON NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_wf_trans_log_inst` (`workflow_instance_id`),
INDEX `idx_wf_trans_log_date` (`created_at`),
CONSTRAINT `fk_wf_trans_log_inst` FOREIGN KEY (`workflow_instance_id`) REFERENCES `workflow_instances`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `workflow_transition_log`",
];
\ No newline at end of file
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment