progress in qa part
Some checks failed
PHP Composer / build (push) Has been cancelled

This commit is contained in:
Hesabix 2025-09-05 20:33:33 +03:30
parent be126d506b
commit 3c7fa1b8a4
8 changed files with 656 additions and 53 deletions

View file

@ -9,6 +9,8 @@ export default class extends Controller {
connect() { connect() {
console.log('Wallet Connect Controller connected'); console.log('Wallet Connect Controller connected');
console.log('Controller targets:', this.targets);
console.log('Controller values:', this.values);
this.selectedWallet = null; this.selectedWallet = null;
this.walletAddress = null; this.walletAddress = null;
this.signature = null; this.signature = null;
@ -130,14 +132,20 @@ export default class extends Controller {
} }
} }
async setPrimaryWallet(walletId) { async setPrimaryWallet(event) {
const walletId = event.currentTarget.dataset.walletId;
if (!walletId) {
this.showMessage('شناسه کیف پول یافت نشد', 'error');
return;
}
if (!confirm('آیا می‌خواهید این کیف پول را به عنوان اصلی تنظیم کنید؟')) { if (!confirm('آیا می‌خواهید این کیف پول را به عنوان اصلی تنظیم کنید؟')) {
return; return;
} }
try { try {
const response = await fetch(`/api/wallet/${walletId}/set-primary`, { const response = await fetch(`/api/wallet/${walletId}/set-primary`, {
method: 'POST', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': this.csrfTokenValue || '' 'X-CSRF-Token': this.csrfTokenValue || ''
@ -161,10 +169,19 @@ export default class extends Controller {
} }
} }
async toggleWalletStatus(walletId) { async toggleWalletStatus(event) {
console.log('toggleWalletStatus called', event);
const walletId = event.currentTarget.dataset.walletId;
console.log('Wallet ID:', walletId);
if (!walletId) {
this.showMessage('شناسه کیف پول یافت نشد', 'error');
return;
}
try { try {
console.log('Making request to toggle wallet status...');
const response = await fetch(`/api/wallet/${walletId}/toggle-status`, { const response = await fetch(`/api/wallet/${walletId}/toggle-status`, {
method: 'POST', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': this.csrfTokenValue || '' 'X-CSRF-Token': this.csrfTokenValue || ''
@ -188,12 +205,21 @@ export default class extends Controller {
} }
} }
async deleteWallet(walletId) { async deleteWallet(event) {
console.log('deleteWallet called', event);
const walletId = event.currentTarget.dataset.walletId;
console.log('Wallet ID:', walletId);
if (!walletId) {
this.showMessage('شناسه کیف پول یافت نشد', 'error');
return;
}
if (!confirm('آیا مطمئن هستید که می‌خواهید این کیف پول را حذف کنید؟')) { if (!confirm('آیا مطمئن هستید که می‌خواهید این کیف پول را حذف کنید؟')) {
return; return;
} }
try { try {
console.log('Making request to delete wallet...');
const response = await fetch(`/api/wallet/${walletId}`, { const response = await fetch(`/api/wallet/${walletId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {

View file

@ -5,8 +5,4 @@ framework:
token_id: submit token_id: submit
csrf_protection: csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout
enabled: true enabled: true

View file

@ -42,6 +42,18 @@ security:
logout: logout:
path: customer_logout path: customer_logout
target: app_home target: app_home
qa:
pattern: ^/qa
lazy: true
provider: app_user_provider
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week
path: /
always_remember_me: false
logout:
path: customer_logout
target: app_home
main: main:
pattern: ^/ pattern: ^/
lazy: true lazy: true
@ -69,6 +81,12 @@ security:
# محافظت از سایر مسیرهای /admin # محافظت از سایر مسیرهای /admin
- { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/customer/dashboard, roles: ROLE_CUSTOMER } - { path: ^/customer/dashboard, roles: ROLE_CUSTOMER }
# محافظت از مسیرهای QA
- { path: ^/qa/ask, roles: ROLE_CUSTOMER }
- { path: ^/qa/question/.*/answer, roles: ROLE_CUSTOMER }
- { path: ^/qa/.*/vote, roles: ROLE_CUSTOMER }
- { path: ^/qa/.*/accept, roles: ROLE_CUSTOMER }
- { path: ^/qa/tag/create, roles: ROLE_CUSTOMER }
# - { path: ^/profile, roles: ROLE_USER } # - { path: ^/profile, roles: ROLE_USER }
when@test: when@test:

View file

@ -136,9 +136,82 @@ class QAController extends AbstractController
$form = $this->createForm(QuestionFormType::class, $question); $form = $this->createForm(QuestionFormType::class, $question);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// مدیریت تگ‌ها
$selectedTagIds = $request->request->all('question')['tags'] ?? []; if ($form->isSubmitted()) {
// بررسی CSRF token - حذف شده چون Symfony خودش بررسی می‌کند
// if (!$this->isCsrfTokenValid('question', $request->request->get('_token'))) {
// $this->addFlash('error', 'CSRF token نامعتبر است.');
// $availableTags = $this->tagRepository->findActiveTags();
// return $this->render('qa/ask_question.html.twig', [
// 'form' => $form,
// 'availableTags' => $availableTags,
// ]);
// }
// دریافت تگ‌های انتخاب شده از فرم
$selectedTags = $form->get('tags')->getData();
$selectedTagIds = [];
if ($selectedTags) {
foreach ($selectedTags as $tag) {
$selectedTagIds[] = $tag->getId();
}
}
// Debug: بررسی تگ‌های ارسال شده
error_log('Selected tag IDs from form: ' . print_r($selectedTagIds, true));
error_log('Form is valid: ' . ($form->isValid() ? 'true' : 'false'));
// اگر form valid نیست، خطاها را نمایش بده
if (!$form->isValid()) {
$this->addFlash('error', 'لطفاً تمام فیلدهای الزامی را پر کنید');
$availableTags = $this->tagRepository->findActiveTags();
return $this->render('qa/ask_question.html.twig', [
'form' => $form,
'availableTags' => $availableTags,
]);
}
// مدیریت پیوست فایل‌ها
$attachments = $form->get('attachments')->getData();
error_log('Attachments from form: ' . print_r($attachments, true));
// بررسی فایل‌های ارسال شده در request
$uploadedFiles = $request->files->get('question_form')['attachments'] ?? [];
error_log('Uploaded files from request: ' . print_r($uploadedFiles, true));
if ($attachments && count($attachments) > 0) {
error_log('Processing ' . count($attachments) . ' attachments from form');
$uploadedAttachments = $this->attachmentService->uploadAttachments($attachments, $this->getUser(), $question);
error_log('Uploaded attachments: ' . print_r($uploadedAttachments, true));
foreach ($uploadedAttachments as $attachment) {
$question->addAttachment($attachment);
error_log('Added attachment to question: ' . $attachment->getOriginalFilename());
}
} elseif ($uploadedFiles && count($uploadedFiles) > 0) {
error_log('Processing ' . count($uploadedFiles) . ' files from request');
$uploadedAttachments = $this->attachmentService->uploadAttachments($uploadedFiles, $this->getUser(), $question);
error_log('Uploaded attachments from request: ' . print_r($uploadedAttachments, true));
foreach ($uploadedAttachments as $attachment) {
$question->addAttachment($attachment);
error_log('Added attachment to question: ' . $attachment->getOriginalFilename());
}
} else {
error_log('No attachments found in form data or request');
}
// حذف تمام تگ‌های قبلی برای این سوال
$existingRelations = $this->entityManager->getRepository(QuestionTagRelation::class)
->findBy(['question' => $question]);
foreach ($existingRelations as $relation) {
$this->entityManager->remove($relation);
}
// اضافه کردن تگ‌های جدید
foreach ($selectedTagIds as $tagId) { foreach ($selectedTagIds as $tagId) {
$tag = $this->tagRepository->find($tagId); $tag = $this->tagRepository->find($tagId);
if ($tag) { if ($tag) {
@ -149,18 +222,13 @@ class QAController extends AbstractController
// افزایش تعداد استفاده از تگ // افزایش تعداد استفاده از تگ
$tag->incrementUsageCount(); $tag->incrementUsageCount();
error_log('Added tag relation for tag ID: ' . $tagId);
} }
} }
// مدیریت پیوست فایل‌ها
$attachments = $form->get('attachments')->getData();
if ($attachments) {
$uploadedAttachments = $this->attachmentService->uploadAttachments($attachments, $this->getUser(), $question);
foreach ($uploadedAttachments as $attachment) {
$question->addAttachment($attachment);
}
}
// ذخیره سوال در دیتابیس
$this->entityManager->persist($question); $this->entityManager->persist($question);
$this->entityManager->flush(); $this->entityManager->flush();
@ -394,7 +462,7 @@ class QAController extends AbstractController
public function createTag(Request $request): JsonResponse public function createTag(Request $request): JsonResponse
{ {
// بررسی CSRF token // بررسی CSRF token
if (!$this->isCsrfTokenValid('vote', $request->request->get('_token'))) { if (!$this->isCsrfTokenValid('question', $request->request->get('_token'))) {
return new JsonResponse(['error' => 'CSRF token نامعتبر است.'], 403); return new JsonResponse(['error' => 'CSRF token نامعتبر است.'], 403);
} }

View file

@ -61,13 +61,19 @@ class QuestionFormType extends AbstractType
->add('tags', EntityType::class, [ ->add('tags', EntityType::class, [
'class' => QuestionTag::class, 'class' => QuestionTag::class,
'choice_label' => 'name', 'choice_label' => 'name',
'choice_value' => 'id',
'multiple' => true, 'multiple' => true,
'expanded' => false, 'expanded' => false,
'required' => false,
'mapped' => false, 'mapped' => false,
'required' => false,
'query_builder' => function (\Doctrine\ORM\EntityRepository $er) {
return $er->createQueryBuilder('t')
->where('t.isActive = :active')
->setParameter('active', true)
->orderBy('t.name', 'ASC');
},
'attr' => [ 'attr' => [
'class' => 'form-control', 'class' => 'form-control d-none' // مخفی می‌کنیم چون با JavaScript مدیریت می‌شود
'style' => 'display: none;' // مخفی کردن چون با JavaScript مدیریت می‌شود
], ],
'constraints' => [ 'constraints' => [
new Assert\Count([ new Assert\Count([

View file

@ -173,21 +173,21 @@
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="flex gap-2"> <div class="flex gap-2">
{% if not wallet.isPrimary and wallet.isActive %} {% if not wallet.isPrimary and wallet.isActive %}
<button class="inline-flex items-center px-3 py-1 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors duration-200" <button class="wallet-set-primary-btn inline-flex items-center px-3 py-1 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors duration-200"
data-wallet-id="{{ wallet.id }}" data-wallet-id="{{ wallet.id }}"
data-action="click->wallet-connect#setPrimaryWallet"> data-action="click->wallet-connect#setPrimaryWallet">
<i class="fas fa-star ml-1"></i> اصلی <i class="fas fa-star ml-1"></i> اصلی
</button> </button>
{% endif %} {% endif %}
<button class="inline-flex items-center px-3 py-1 text-xs font-medium text-yellow-600 bg-yellow-50 border border-yellow-200 rounded-lg hover:bg-yellow-100 transition-colors duration-200" <button class="wallet-toggle-btn inline-flex items-center px-3 py-1 text-xs font-medium text-yellow-600 bg-yellow-50 border border-yellow-200 rounded-lg hover:bg-yellow-100 transition-colors duration-200"
data-wallet-id="{{ wallet.id }}" data-wallet-id="{{ wallet.id }}"
data-action="click->wallet-connect#toggleWalletStatus"> data-action="click->wallet-connect#toggleWalletStatus">
<i class="fas fa-{{ wallet.isActive ? 'pause' : 'play' }} ml-1"></i> <i class="fas fa-{{ wallet.isActive ? 'pause' : 'play' }} ml-1"></i>
{{ wallet.isActive ? 'غیرفعال' : 'فعال' }} {{ wallet.isActive ? 'غیرفعال' : 'فعال' }}
</button> </button>
<button class="inline-flex items-center px-3 py-1 text-xs font-medium text-red-600 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors duration-200" <button class="wallet-delete-btn inline-flex items-center px-3 py-1 text-xs font-medium text-red-600 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors duration-200"
data-wallet-id="{{ wallet.id }}" data-wallet-id="{{ wallet.id }}"
data-action="click->wallet-connect#deleteWallet"> data-action="click->wallet-connect#deleteWallet">
<i class="fas fa-trash ml-1"></i> حذف <i class="fas fa-trash ml-1"></i> حذف
@ -244,4 +244,400 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script>
// Beautiful Dialog System
class BeautifulDialog {
constructor() {
this.container = null;
this.init();
}
init() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.createContainer();
});
} else {
this.createContainer();
}
}
createContainer() {
// Create dialog container if it doesn't exist
this.container = document.getElementById('dialog-container');
if (!this.container && document.body) {
this.container = document.createElement('div');
this.container.id = 'dialog-container';
this.container.className = 'fixed inset-0 z-50 overflow-y-auto';
this.container.style.display = 'none';
document.body.appendChild(this.container);
}
}
showConfirm(title, message, confirmText = 'تأیید', cancelText = 'انصراف', onConfirm = null, onCancel = null) {
// Ensure container exists
if (!this.container) {
this.createContainer();
}
if (!this.container) {
console.error('Dialog container not available');
return;
}
const dialogId = 'confirm-dialog-' + Date.now();
const dialogHtml = `
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4" id="${dialogId}">
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full transform transition-all duration-300 scale-95 opacity-0" id="${dialogId}-content">
<div class="p-6">
<div class="flex items-center mb-4">
<div class="flex-shrink-0 w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<div class="mr-3">
<h3 class="text-lg font-semibold text-gray-900">${title}</h3>
</div>
</div>
<div class="mb-6">
<p class="text-sm text-gray-600 leading-relaxed">${message}</p>
</div>
<div class="flex justify-end space-x-3 space-x-reverse">
<button type="button" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors duration-200" id="${dialogId}-cancel">
${cancelText}
</button>
<button type="button" class="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 transition-colors duration-200" id="${dialogId}-confirm">
${confirmText}
</button>
</div>
</div>
</div>
</div>
`;
this.container.innerHTML = dialogHtml;
this.container.style.display = 'block';
// Animate in
setTimeout(() => {
const content = document.getElementById(`${dialogId}-content`);
content.classList.remove('scale-95', 'opacity-0');
content.classList.add('scale-100', 'opacity-100');
}, 10);
// Event listeners
document.getElementById(`${dialogId}-confirm`).addEventListener('click', () => {
this.hide();
if (onConfirm) onConfirm();
});
document.getElementById(`${dialogId}-cancel`).addEventListener('click', () => {
this.hide();
if (onCancel) onCancel();
});
// Close on backdrop click
document.getElementById(dialogId).addEventListener('click', (e) => {
if (e.target.id === dialogId) {
this.hide();
if (onCancel) onCancel();
}
});
}
showSuccess(title, message, duration = 3000) {
this.showNotification(title, message, 'success', duration);
}
showError(title, message, duration = 5000) {
this.showNotification(title, message, 'error', duration);
}
showInfo(title, message, duration = 4000) {
this.showNotification(title, message, 'info', duration);
}
showNotification(title, message, type = 'info', duration = 4000) {
// Ensure document.body is available
if (!document.body) {
console.error('Document body not available');
return;
}
const notificationId = 'notification-' + Date.now();
const icons = {
success: `<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>`,
error: `<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>`,
info: `<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>`
};
const colors = {
success: 'bg-green-50 border-green-200',
error: 'bg-red-50 border-red-200',
info: 'bg-blue-50 border-blue-200'
};
const notificationHtml = `
<div class="fixed top-4 right-4 z-50 transform transition-all duration-300 translate-x-full opacity-0" id="${notificationId}">
<div class="max-w-sm w-full ${colors[type]} border rounded-lg shadow-lg">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
${icons[type]}
</div>
<div class="mr-3 w-0 flex-1">
<p class="text-sm font-medium text-gray-900">${title}</p>
<p class="mt-1 text-sm text-gray-600">${message}</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button class="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none" onclick="document.getElementById('${notificationId}').remove()">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', notificationHtml);
// Animate in
setTimeout(() => {
const notification = document.getElementById(notificationId);
notification.classList.remove('translate-x-full', 'opacity-0');
notification.classList.add('translate-x-0', 'opacity-100');
}, 10);
// Auto remove
if (duration > 0) {
setTimeout(() => {
const notification = document.getElementById(notificationId);
if (notification) {
notification.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => {
notification.remove();
}, 300);
}
}, duration);
}
}
hide() {
this.container.style.display = 'none';
this.container.innerHTML = '';
}
}
// Initialize dialog system after DOM is ready
let dialog;
document.addEventListener('DOMContentLoaded', function() {
dialog = new BeautifulDialog();
});
// Helper function to show dialogs with fallback
function showDialog(type, title, message, ...args) {
if (dialog && dialog[type]) {
dialog[type](title, message, ...args);
} else {
// Fallback to alert
alert(`${title}: ${message}`);
}
}
// Wallet Connect Controller
document.addEventListener('DOMContentLoaded', function() {
console.log('Wallet Connect script loaded');
// Handle wallet type change
const walletTypeSelect = document.querySelector('[data-wallet-connect-target="walletType"]');
if (walletTypeSelect) {
walletTypeSelect.addEventListener('change', function() {
const connectBtn = document.querySelector('[data-wallet-connect-target="connectBtn"]');
if (this.value) {
connectBtn.disabled = false;
connectBtn.textContent = 'اتصال کیف پول';
} else {
connectBtn.disabled = true;
connectBtn.textContent = 'نوع کیف پول را انتخاب کنید';
}
});
}
// Handle toggle wallet status
document.addEventListener('click', function(event) {
if (event.target.closest('.wallet-toggle-btn')) {
event.preventDefault();
const button = event.target.closest('.wallet-toggle-btn');
const walletId = button.dataset.walletId;
console.log('Toggle wallet status clicked, ID:', walletId);
if (!walletId) {
showDialog('showError', 'خطا', 'شناسه کیف پول یافت نشد');
return;
}
const isActive = button.textContent.includes('غیرفعال');
const action = isActive ? 'غیرفعال' : 'فعال';
showDialog('showConfirm',
'تأیید تغییر وضعیت',
`آیا مطمئن هستید که می‌خواهید این کیف پول را ${action} کنید؟`,
'تأیید',
'انصراف',
() => {
// Show loading
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin ml-1"></i> در حال پردازش...';
fetch(`/api/wallet/${walletId}/toggle-status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': '{{ csrf_token('wallet_connect_form') }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showDialog('showSuccess', 'موفقیت', data.message);
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showDialog('showError', 'خطا', data.message);
button.disabled = false;
button.innerHTML = `<i class="fas fa-${isActive ? 'pause' : 'play'} ml-1"></i> ${isActive ? 'غیرفعال' : 'فعال'}`;
}
})
.catch(error => {
console.error('Error:', error);
showDialog('showError', 'خطا', 'خطا در تغییر وضعیت کیف پول: ' + error.message);
button.disabled = false;
button.innerHTML = `<i class="fas fa-${isActive ? 'pause' : 'play'} ml-1"></i> ${isActive ? 'غیرفعال' : 'فعال'}`;
});
}
);
}
});
// Handle delete wallet
document.addEventListener('click', function(event) {
if (event.target.closest('.wallet-delete-btn')) {
event.preventDefault();
const button = event.target.closest('.wallet-delete-btn');
const walletId = button.dataset.walletId;
console.log('Delete wallet clicked, ID:', walletId);
if (!walletId) {
showDialog('showError', 'خطا', 'شناسه کیف پول یافت نشد');
return;
}
showDialog('showConfirm',
'تأیید حذف کیف پول',
'آیا مطمئن هستید که می‌خواهید این کیف پول را حذف کنید؟ این عمل قابل بازگشت نیست.',
'حذف',
'انصراف',
() => {
// Show loading
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin ml-1"></i> در حال حذف...';
fetch(`/api/wallet/${walletId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': '{{ csrf_token('wallet_connect_form') }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showDialog('showSuccess', 'موفقیت', data.message);
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showDialog('showError', 'خطا', data.message);
button.disabled = false;
button.innerHTML = '<i class="fas fa-trash ml-1"></i> حذف';
}
})
.catch(error => {
console.error('Error:', error);
showDialog('showError', 'خطا', 'خطا در حذف کیف پول: ' + error.message);
button.disabled = false;
button.innerHTML = '<i class="fas fa-trash ml-1"></i> حذف';
});
}
);
}
});
// Handle set primary wallet
document.addEventListener('click', function(event) {
if (event.target.closest('.wallet-set-primary-btn')) {
event.preventDefault();
const button = event.target.closest('.wallet-set-primary-btn');
const walletId = button.dataset.walletId;
console.log('Set primary wallet clicked, ID:', walletId);
if (!walletId) {
showDialog('showError', 'خطا', 'شناسه کیف پول یافت نشد');
return;
}
showDialog('showConfirm',
'تنظیم کیف پول اصلی',
'آیا می‌خواهید این کیف پول را به عنوان کیف پول اصلی تنظیم کنید؟',
'تأیید',
'انصراف',
() => {
// Show loading
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin ml-1"></i> در حال تنظیم...';
fetch(`/api/wallet/${walletId}/set-primary`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': '{{ csrf_token('wallet_connect_form') }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showDialog('showSuccess', 'موفقیت', data.message);
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showDialog('showError', 'خطا', data.message);
button.disabled = false;
button.innerHTML = '<i class="fas fa-star ml-1"></i> اصلی';
}
})
.catch(error => {
console.error('Error:', error);
showDialog('showError', 'خطا', 'خطا در تنظیم کیف پول اصلی: ' + error.message);
button.disabled = false;
button.innerHTML = '<i class="fas fa-star ml-1"></i> اصلی';
});
}
);
}
});
});
</script>
{% endblock %} {% endblock %}

View file

@ -19,7 +19,7 @@
<!-- فرم ایجاد سوال --> <!-- فرم ایجاد سوال -->
<div class="bg-white rounded-2xl shadow-soft overflow-hidden"> <div class="bg-white rounded-2xl shadow-soft overflow-hidden">
<div class="p-8"> <div class="p-8">
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-8', 'data-turbo': 'false'}}) }} {{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-8', 'data-turbo': 'false', 'enctype': 'multipart/form-data'}}) }}
<!-- عنوان سوال --> <!-- عنوان سوال -->
<div> <div>
@ -98,6 +98,11 @@
تگ‌ها تگ‌ها
<span class="text-red-500">*</span> <span class="text-red-500">*</span>
</label> </label>
<!-- فیلد مخفی Symfony برای تگ‌ها -->
{{ form_widget(form.tags, {'attr': {'class': 'hidden'}}) }}
{{ form_errors(form.tags) }}
<div class="border border-gray-300 rounded-xl p-6 bg-gradient-to-br from-gray-50 to-blue-50"> <div class="border border-gray-300 rounded-xl p-6 bg-gradient-to-br from-gray-50 to-blue-50">
<!-- تگ‌های انتخاب شده --> <!-- تگ‌های انتخاب شده -->
<div class="mb-6"> <div class="mb-6">
@ -364,10 +369,37 @@ window.QuestionFormClasses.TagManager = class TagManager {
} }
this.bindEvents(); this.bindEvents();
this.loadExistingTags();
this.updateTagCount(); this.updateTagCount();
this.renderSelectedTags(); this.renderSelectedTags();
} }
loadExistingTags() {
// بارگذاری تگ‌های موجود از فرم Symfony
const form = document.querySelector('form');
if (!form) return;
const tagsField = form.querySelector('select[name="question_form[tags][]"]') ||
form.querySelector('select[id*="tags"]');
if (!tagsField) return;
// اطمینان از اینکه فیلد multiple است
tagsField.multiple = true;
const selectedOptions = tagsField.querySelectorAll('option:checked');
selectedOptions.forEach(option => {
const tagId = option.value;
const tagName = option.textContent;
// بررسی اینکه تگ قبلاً اضافه نشده باشد
if (!this.selectedTags.find(tag => tag.id === tagId)) {
this.selectedTags.push({ id: tagId, name: tagName });
}
});
console.log('Loaded existing tags:', this.selectedTags);
}
bindEvents() { bindEvents() {
// حذف event listener های قبلی // حذف event listener های قبلی
this.removeEventListeners(); this.removeEventListeners();
@ -518,7 +550,7 @@ window.QuestionFormClasses.TagManager = class TagManager {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
}, },
body: `name=${encodeURIComponent(tagName)}&_token={{ csrf_token('vote') }}` body: `name=${encodeURIComponent(tagName)}&_token={{ csrf_token('question') }}`
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
@ -644,18 +676,36 @@ window.QuestionFormClasses.TagManager = class TagManager {
} }
updateHiddenInput() { updateHiddenInput() {
// حذف input های قبلی const form = document.querySelector('form');
const existingInputs = document.querySelectorAll('input[name="question[tags][]"]'); if (!form) {
existingInputs.forEach(input => input.remove()); console.error('Form not found for updating hidden inputs');
return;
}
// افزودن input های جدید // پیدا کردن فیلد tags در فرم Symfony
const tagsField = form.querySelector('select[name="question_form[tags][]"]') ||
form.querySelector('select[id*="tags"]');
if (!tagsField) {
console.error('Tags field not found in form');
return;
}
// اطمینان از اینکه فیلد multiple است
tagsField.multiple = true;
// پاک کردن گزینه‌های قبلی
tagsField.innerHTML = '';
// افزودن گزینه‌های جدید
this.selectedTags.forEach(tag => { this.selectedTags.forEach(tag => {
const input = document.createElement('input'); const option = document.createElement('option');
input.type = 'hidden'; option.value = tag.id;
input.name = 'question[tags][]'; option.textContent = tag.name;
input.value = tag.id; option.selected = true;
document.querySelector('form').appendChild(input); tagsField.appendChild(option);
}); });
console.log('Updated tags field with', this.selectedTags.length, 'tags');
} }
filterSuggestions() { filterSuggestions() {
@ -794,16 +844,22 @@ window.QuestionFormClasses.FileManager = class FileManager {
} }
init() { init() {
this.fileInput = document.getElementById('file-input'); // پیدا کردن input file - ممکن است multiple باشد
this.fileInput = document.querySelector('input[type="file"][name*="attachments"]');
this.selectedFilesDiv = document.getElementById('selected-files'); this.selectedFilesDiv = document.getElementById('selected-files');
this.fileListDiv = document.getElementById('file-list'); this.fileListDiv = document.getElementById('file-list');
if (!this.fileInput) return; if (!this.fileInput) {
console.error('File input not found');
return;
}
this.bindEvents(); this.bindEvents();
} }
bindEvents() { bindEvents() {
if (!this.fileInput) return;
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e)); this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
// Drag and drop // Drag and drop
@ -845,6 +901,11 @@ window.QuestionFormClasses.FileManager = class FileManager {
} }
}); });
this.updateFileInput(); this.updateFileInput();
// اطمینان از اینکه فایل‌ها در input قرار گرفته‌اند
setTimeout(() => {
this.updateFileInput();
}, 100);
} }
validateFile(file) { validateFile(file) {
@ -886,6 +947,9 @@ window.QuestionFormClasses.FileManager = class FileManager {
}); });
this.renderFiles(); this.renderFiles();
// اطمینان از اینکه فایل به input اضافه شده است
this.updateFileInput();
} }
removeFile(fileId) { removeFile(fileId) {
@ -950,12 +1014,27 @@ window.QuestionFormClasses.FileManager = class FileManager {
} }
updateFileInput() { updateFileInput() {
// ایجاد DataTransfer object جدید // بررسی وجود fileInput
const dt = new DataTransfer(); if (!this.fileInput) {
this.selectedFiles.forEach(fileData => { console.error('File input is null, cannot update files');
dt.items.add(fileData.file); return;
}); }
this.fileInput.files = dt.files;
// تنظیم فایل‌ها در input اصلی
try {
const dt = new DataTransfer();
this.selectedFiles.forEach(fileData => {
dt.items.add(fileData.file);
});
this.fileInput.files = dt.files;
console.log('Updated file input with', this.selectedFiles.length, 'files');
console.log('File input files:', this.fileInput.files.length);
} catch (e) {
console.warn('DataTransfer not supported:', e);
// در صورت عدم پشتیبانی، فایل‌ها را در متغیر سراسری ذخیره می‌کنیم
window.selectedFiles = this.selectedFiles;
}
} }
} }
} }
@ -1023,6 +1102,7 @@ function initializeQuestionForm() {
function handleFormSubmit(e) { function handleFormSubmit(e) {
// اطمینان از اینکه تگ‌ها به فرم اضافه شده‌اند // اطمینان از اینکه تگ‌ها به فرم اضافه شده‌اند
if (window.questionFormManager.tagManager) { if (window.questionFormManager.tagManager) {
// ابتدا تگ‌ها را به فرم اضافه کن
window.questionFormManager.tagManager.updateHiddenInput(); window.questionFormManager.tagManager.updateHiddenInput();
// اعتبارسنجی تگ‌ها // اعتبارسنجی تگ‌ها
@ -1033,11 +1113,24 @@ function handleFormSubmit(e) {
} }
} }
// اطمینان از اینکه فایل‌ها به فرم اضافه شده‌اند
if (window.questionFormManager.fileManager) {
window.questionFormManager.fileManager.updateFileInput();
}
// غیرفعال کردن Turbo برای این فرم // غیرفعال کردن Turbo برای این فرم
const form = e.target; const form = e.target;
if (form) { if (form) {
form.setAttribute('data-turbo', 'false'); form.setAttribute('data-turbo', 'false');
} }
const fileInput = form.querySelector('input[type="file"]');
console.log('Form submitted with files:', fileInput ? fileInput.files.length : 0);
// بررسی فایل‌های انتخاب شده در JavaScript
if (window.questionFormManager.fileManager) {
console.log('Selected files in JavaScript:', window.questionFormManager.fileManager.selectedFiles.length);
}
} }
// اجرا در بارگذاری صفحه // اجرا در بارگذاری صفحه