Buy Sell
This commit is contained in:
@@ -362,6 +362,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно продажи актива -->
|
||||
<div id="sell-asset-modal" class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<h3 id="sell-asset-title">Продажа актива</h3>
|
||||
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
||||
<span>Текущая цена:</span>
|
||||
<strong id="sell-asset-price">0 ₽</strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
||||
<span>У вас в наличии:</span>
|
||||
<strong id="sell-player-quantity">0 шт.</strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||
<span>Комиссия:</span>
|
||||
<strong id="sell-fee">1%</strong>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="sell-quantity">Количество для продажи</label>
|
||||
<input type="number" id="sell-quantity" min="1" value="1" style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>Выручка:</span>
|
||||
<strong id="sell-revenue">0 ₽</strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
|
||||
<span>Комиссия:</span>
|
||||
<strong id="sell-fee-amount">0 ₽</strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
|
||||
<span>К получению:</span>
|
||||
<strong id="sell-total">0 ₽</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button onclick="confirmSellAsset()" class="button warning" style="flex: 1;">
|
||||
Продать
|
||||
</button>
|
||||
<button onclick="cancelSellAsset()" class="button secondary" style="flex: 1;">
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -374,6 +425,7 @@ let currentPhase = '{{ game_state.phase }}';
|
||||
let phaseEndTime = '{{ game_state.phase_end }}';
|
||||
let selectedAsset = null;
|
||||
let gameTimer = null;
|
||||
let selectedAssetForSell = null;
|
||||
|
||||
// Инициализация при загрузке
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -643,7 +695,8 @@ function loadAssets() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.assets) {
|
||||
displayAssets(data.assets);
|
||||
window.allAssetsData = data.assets;
|
||||
displayAssets(data.assets, data.categories);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -652,37 +705,194 @@ function loadAssets() {
|
||||
});
|
||||
}
|
||||
|
||||
function displayAssets(assets) {
|
||||
function displayAssets(assets, categories) {
|
||||
const assetsList = document.getElementById('assets-list');
|
||||
if (!assetsList) return;
|
||||
|
||||
assetsList.innerHTML = '';
|
||||
|
||||
Object.entries(assets).forEach(([assetId, assetData]) => {
|
||||
const assetElement = document.createElement('div');
|
||||
assetElement.className = 'asset-item';
|
||||
assetElement.innerHTML = `
|
||||
<div class="asset-info">
|
||||
<div class="asset-name">
|
||||
${getAssetName(assetId)}
|
||||
</div>
|
||||
<div class="asset-meta">
|
||||
Волатильность: ${(assetData.volatility * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div class="asset-price">
|
||||
${formatCurrency(assetData.price)}
|
||||
</div>
|
||||
<button onclick="showBuyAssetModal('${assetId}', ${assetData.price})"
|
||||
class="button" style="margin-top: 5px; padding: 5px 10px; font-size: 0.9rem;">
|
||||
Купить
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
// Сохраняем категории глобально для использования в других функциях
|
||||
window.assetCategories = categories || {
|
||||
'bonds': {'name': 'Облигации', 'icon': '🏦'},
|
||||
'stocks': {'name': 'Акции', 'icon': '📈'},
|
||||
'real_estate': {'name': 'Недвижимость', 'icon': '🏠'},
|
||||
'crypto': {'name': 'Криптовалюта', 'icon': '💰'},
|
||||
'commodities': {'name': 'Сырьевые товары', 'icon': '⛽'},
|
||||
'business': {'name': 'Бизнес', 'icon': '🏢'},
|
||||
'unique': {'name': 'Уникальные активы', 'icon': '🏆'}
|
||||
};
|
||||
|
||||
assetsList.appendChild(assetElement);
|
||||
// Группируем активы по категориям
|
||||
const groupedAssets = {};
|
||||
|
||||
Object.entries(assets).forEach(([assetId, assetData]) => {
|
||||
const category = assetData.category || 'other';
|
||||
if (!groupedAssets[category]) {
|
||||
groupedAssets[category] = [];
|
||||
}
|
||||
groupedAssets[category].push({ id: assetId, ...assetData });
|
||||
});
|
||||
|
||||
// Порядок категорий для отображения
|
||||
const categoryOrder = ['bonds', 'stocks', 'real_estate', 'business', 'commodities', 'crypto', 'unique'];
|
||||
|
||||
// Отображаем активы по категориям в заданном порядке
|
||||
categoryOrder.forEach(categoryKey => {
|
||||
if (groupedAssets[categoryKey] && groupedAssets[categoryKey].length > 0) {
|
||||
const category = window.assetCategories[categoryKey] || { name: categoryKey, icon: '📊' };
|
||||
|
||||
// Заголовок категории
|
||||
const categoryHeader = document.createElement('div');
|
||||
categoryHeader.className = 'category-header';
|
||||
categoryHeader.innerHTML = `
|
||||
<span style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 1.3rem;">${category.icon || '📊'}</span>
|
||||
<span style="font-weight: 600; color: var(--primary-color);">${category.name || categoryKey}</span>
|
||||
</span>
|
||||
`;
|
||||
assetsList.appendChild(categoryHeader);
|
||||
|
||||
// Сортируем активы внутри категории по цене (от дешевых к дорогим)
|
||||
groupedAssets[categoryKey].sort((a, b) => a.price - b.price);
|
||||
|
||||
// Активы категории
|
||||
groupedAssets[categoryKey].forEach(asset => {
|
||||
const assetElement = createAssetElement(asset);
|
||||
assetsList.appendChild(assetElement);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Показываем оставшиеся категории, которые не вошли в порядок
|
||||
Object.keys(groupedAssets).forEach(categoryKey => {
|
||||
if (!categoryOrder.includes(categoryKey) && groupedAssets[categoryKey].length > 0) {
|
||||
const category = window.assetCategories[categoryKey] || { name: categoryKey, icon: '📊' };
|
||||
|
||||
const categoryHeader = document.createElement('div');
|
||||
categoryHeader.className = 'category-header';
|
||||
categoryHeader.innerHTML = `
|
||||
<span style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 1.3rem;">${category.icon || '📊'}</span>
|
||||
<span style="font-weight: 600; color: var(--primary-color);">${category.name || categoryKey}</span>
|
||||
</span>
|
||||
`;
|
||||
assetsList.appendChild(categoryHeader);
|
||||
|
||||
groupedAssets[categoryKey].forEach(asset => {
|
||||
const assetElement = createAssetElement(asset);
|
||||
assetsList.appendChild(assetElement);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createAssetElement(asset) {
|
||||
const assetElement = document.createElement('div');
|
||||
assetElement.className = 'asset-item';
|
||||
|
||||
const isAvailable = asset.available === null || asset.available > 0;
|
||||
const canAfford = window.playerCapital >= asset.price * asset.min_purchase;
|
||||
const canBuyMore = !asset.max_per_player || asset.player_quantity < asset.max_per_player;
|
||||
|
||||
let availabilityText = '';
|
||||
if (asset.available !== null) {
|
||||
const percentage = ((asset.available / asset.total) * 100).toFixed(0);
|
||||
availabilityText = `<div style="font-size: 0.8rem; color: var(--light-text); margin-top: 3px;">
|
||||
Доступно: <span style="font-weight: 600; color: ${percentage > 30 ? 'var(--success-color)' : 'var(--warning-color)'}">
|
||||
${asset.available} / ${asset.total}
|
||||
</span> (${percentage}%)
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let playerQuantityText = '';
|
||||
if (asset.player_quantity > 0) {
|
||||
playerQuantityText = `<div style="font-size: 0.8rem; color: var(--success-color); margin-top: 3px;">
|
||||
У вас: <span style="font-weight: 600;">${asset.player_quantity} шт.</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let limitText = '';
|
||||
if (asset.max_per_player) {
|
||||
limitText = `<div style="font-size: 0.8rem; color: var(--light-text); margin-top: 3px;">
|
||||
Лимит: ${asset.player_quantity} / ${asset.max_per_player}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const volatilityLevel = getVolatilityLevel(asset.volatility);
|
||||
const volatilityColors = {
|
||||
'low': '#4CAF50',
|
||||
'medium': '#FF9800',
|
||||
'high': '#F44336'
|
||||
};
|
||||
|
||||
assetElement.innerHTML = `
|
||||
<div class="asset-info" style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 5px;">
|
||||
<span style="font-size: 1.2rem;">${getAssetIcon(asset.category)}</span>
|
||||
<span class="asset-name" style="font-size: 1.1rem; font-weight: 600;">${asset.name}</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 5px;">
|
||||
<span style="font-size: 0.8rem; padding: 3px 8px; border-radius: 12px; background-color: ${volatilityColors[volatilityLevel]}20; color: ${volatilityColors[volatilityLevel]}; font-weight: 600;">
|
||||
⚡ Волатильность: ${(asset.volatility * 100).toFixed(0)}%
|
||||
</span>
|
||||
${asset.income_per_month > 0 ?
|
||||
`<span style="font-size: 0.8rem; padding: 3px 8px; border-radius: 12px; background-color: var(--success-color)20; color: var(--success-color); font-weight: 600;">
|
||||
💰 Доход: +${(asset.income_per_month * 100).toFixed(1)}%/мес
|
||||
</span>` : ''
|
||||
}
|
||||
</div>
|
||||
${availabilityText}
|
||||
${playerQuantityText}
|
||||
${limitText}
|
||||
<div style="font-size: 0.85rem; color: var(--light-text); margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee;">
|
||||
${asset.description || 'Нет описания'}
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right; min-width: 120px;">
|
||||
<div style="font-size: 1.3rem; font-weight: 700; color: var(--primary-color); margin-bottom: 10px;">
|
||||
${formatCurrency(asset.price)}
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
${asset.player_quantity > 0 ?
|
||||
`<button onclick="showSellModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.player_quantity})"
|
||||
class="button danger" style="padding: 8px 12px; font-size: 0.9rem; width: 100%;">
|
||||
📉 Продать
|
||||
</button>` : ''
|
||||
}
|
||||
${isAvailable && canAfford && canBuyMore ?
|
||||
`<button onclick="showBuyAssetModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.min_purchase || 1}, ${asset.available || 'null'})"
|
||||
class="button success" style="padding: 8px 12px; font-size: 0.9rem; width: 100%;">
|
||||
📈 Купить
|
||||
</button>` :
|
||||
`<button disabled class="button" style="padding: 8px 12px; font-size: 0.9rem; width: 100%; opacity: 0.5; cursor: not-allowed;">
|
||||
${!isAvailable ? 'Нет в наличии' : !canAfford ? 'Не хватает средств' : 'Лимит исчерпан'}
|
||||
</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return assetElement;
|
||||
}
|
||||
|
||||
function getAssetIcon(category) {
|
||||
const icons = {
|
||||
'bonds': '🏦',
|
||||
'stocks': '📈',
|
||||
'real_estate': '🏠',
|
||||
'crypto': '💰',
|
||||
'commodities': '⛽',
|
||||
'business': '🏢',
|
||||
'unique': '🏆',
|
||||
'other': '📊'
|
||||
};
|
||||
return icons[category] || '📊';
|
||||
}
|
||||
|
||||
function getVolatilityLevel(volatility) {
|
||||
if (volatility < 0.15) return 'low';
|
||||
if (volatility < 0.25) return 'medium';
|
||||
return 'high';
|
||||
}
|
||||
|
||||
function getAssetName(assetId) {
|
||||
@@ -695,18 +905,40 @@ function getAssetName(assetId) {
|
||||
return names[assetId] || assetId;
|
||||
}
|
||||
|
||||
function showBuyAssetModal(assetId, price) {
|
||||
selectedAsset = { id: assetId, price: price };
|
||||
function showBuyAssetModal(assetId, price, assetName, minQuantity, available) {
|
||||
selectedAsset = {
|
||||
id: assetId,
|
||||
price: price,
|
||||
name: assetName,
|
||||
minQuantity: minQuantity,
|
||||
available: available
|
||||
};
|
||||
|
||||
document.getElementById('buy-asset-title').textContent = `Покупка ${getAssetName(assetId)}`;
|
||||
document.getElementById('buy-asset-title').textContent = `Покупка: ${assetName}`;
|
||||
document.getElementById('asset-price').textContent = formatCurrency(price);
|
||||
document.getElementById('player-capital-display').textContent = formatCurrency(playerCapital);
|
||||
|
||||
const maxQuantity = Math.floor(playerCapital / price);
|
||||
document.getElementById('max-quantity').textContent = `${maxQuantity} шт.`;
|
||||
const maxByCapital = Math.floor(playerCapital / price);
|
||||
let maxQuantity = maxByCapital;
|
||||
|
||||
// Учитываем доступность
|
||||
if (available !== null && available !== undefined) {
|
||||
maxQuantity = Math.min(maxQuantity, available);
|
||||
}
|
||||
|
||||
// Учитываем лимит на игрока
|
||||
const assetData = allAssetsData?.[assetId];
|
||||
if (assetData?.max_per_player) {
|
||||
const currentQuantity = assetData.player_quantity || 0;
|
||||
const remainingLimit = assetData.max_per_player - currentQuantity;
|
||||
maxQuantity = Math.min(maxQuantity, remainingLimit);
|
||||
}
|
||||
|
||||
document.getElementById('max-quantity').textContent = `${maxQuantity} шт.`;
|
||||
document.getElementById('buy-quantity').max = maxQuantity;
|
||||
document.getElementById('buy-quantity').value = 1;
|
||||
document.getElementById('buy-quantity').value = Math.min(minQuantity, maxQuantity);
|
||||
document.getElementById('buy-quantity').min = minQuantity;
|
||||
document.getElementById('buy-quantity').step = minQuantity;
|
||||
|
||||
updateBuyCalculations();
|
||||
|
||||
@@ -846,6 +1078,102 @@ function useAbility() {
|
||||
});
|
||||
}
|
||||
|
||||
//продажа активов
|
||||
function showSellModal(assetId, price, assetName, maxQuantity) {
|
||||
selectedAssetForSell = {
|
||||
id: assetId,
|
||||
price: price,
|
||||
name: assetName,
|
||||
maxQuantity: maxQuantity
|
||||
};
|
||||
|
||||
document.getElementById('sell-asset-title').textContent = `Продажа: ${assetName}`;
|
||||
document.getElementById('sell-asset-price').textContent = formatCurrency(price);
|
||||
document.getElementById('sell-player-quantity').textContent = `${maxQuantity} шт.`;
|
||||
|
||||
const feeRate = 0.01; // 1% комиссия
|
||||
document.getElementById('sell-fee').textContent = `${feeRate * 100}%`;
|
||||
|
||||
document.getElementById('sell-quantity').max = maxQuantity;
|
||||
document.getElementById('sell-quantity').value = 1;
|
||||
document.getElementById('sell-quantity').min = 1;
|
||||
|
||||
updateSellCalculations();
|
||||
|
||||
document.getElementById('sell-asset-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function updateSellCalculations() {
|
||||
if (!selectedAssetForSell) return;
|
||||
|
||||
const quantity = parseInt(document.getElementById('sell-quantity').value) || 1;
|
||||
const price = selectedAssetForSell.price;
|
||||
const feeRate = 0.01;
|
||||
|
||||
const revenue = quantity * price;
|
||||
const fee = revenue * feeRate;
|
||||
const total = revenue - fee;
|
||||
|
||||
document.getElementById('sell-revenue').textContent = formatCurrency(revenue);
|
||||
document.getElementById('sell-fee-amount').textContent = formatCurrency(fee);
|
||||
document.getElementById('sell-total').textContent = formatCurrency(total);
|
||||
}
|
||||
|
||||
function cancelSellAsset() {
|
||||
selectedAssetForSell = null;
|
||||
document.getElementById('sell-asset-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function confirmSellAsset() {
|
||||
if (!selectedAssetForSell) return;
|
||||
|
||||
const quantity = parseInt(document.getElementById('sell-quantity').value) || 1;
|
||||
|
||||
if (quantity > selectedAssetForSell.maxQuantity) {
|
||||
showNotification(`❌ У вас только ${selectedAssetForSell.maxQuantity} шт.`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/game/${roomCode}/sell`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
asset_id: selectedAssetForSell.id,
|
||||
quantity: quantity
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, 'success');
|
||||
cancelSellAsset();
|
||||
|
||||
// Обновляем капитал
|
||||
playerCapital = data.new_capital;
|
||||
updateCapitalProgress();
|
||||
|
||||
// Перезагружаем активы
|
||||
loadAssets();
|
||||
|
||||
// Обновляем отображение капитала
|
||||
document.getElementById('player-capital-display').textContent =
|
||||
formatCurrency(playerCapital);
|
||||
} else {
|
||||
showNotification('❌ ' + data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error selling asset:', error);
|
||||
showNotification('❌ Ошибка при продаже', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем обработчик input для поля количества
|
||||
document.getElementById('sell-quantity')?.addEventListener('input', updateSellCalculations);
|
||||
|
||||
// Завершение фазы действий
|
||||
function endActionPhase() {
|
||||
if (!confirm('Завершить ваши действия и перейти к следующей фазе?')) {
|
||||
@@ -1270,6 +1598,112 @@ document.addEventListener('keydown', function(event) {
|
||||
font-size: 0.9rem;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
/* Стили для категорий активов */
|
||||
.category-header {
|
||||
margin: 25px 0 15px 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-header:first-of-type {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.category-header span {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
margin-bottom: 10px;
|
||||
background-color: white;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #eee;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.asset-item:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: var(--box-shadow);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
flex: 1;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
color: var(--text-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.asset-price {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.volatility-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.volatility-low {
|
||||
background-color: #e8f5e9;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
.volatility-medium {
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.volatility-high {
|
||||
background-color: #ffebee;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.income-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
background-color: #e8f5e9;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.asset-item {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.asset-item > div:last-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user