{% extends "base.twig" %}
{% block title %}{{ t('ui.user_profile.title_with_username', {'username': profile_username}, 'common') }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-user"></i>
{{ t('ui.user_profile.title_with_username', {'username': profile_username}, 'common') }}
</h2>
{% if is_own_profile %}
<a href="/profile" class="btn btn-sm btn-fidonet">
<i class="fas fa-edit"></i>
{{ t('ui.user_profile.edit_my_profile', {}, 'common') }}
</a>
{% endif %}
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">{{ t('ui.user_profile.user_information', {}, 'common') }}</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">{{ t('ui.user_profile.username', {}, 'common') }}:</dt>
<dd class="col-sm-9">{{ profile_username }}</dd>
{% if can_view_sensitive %}
<dt class="col-sm-3">{{ t('ui.user_profile.real_name', {}, 'common') }}:</dt>
<dd class="col-sm-9">{{ profile_real_name|default(t('ui.user_profile.not_specified', {}, 'common')) }}</dd>
{% endif %}
{% if profile_location %}
<dt class="col-sm-3">{{ t('ui.user_profile.location', {}, 'common') }}:</dt>
<dd class="col-sm-9">
<i class="fas fa-map-marker-alt me-1"></i>
{{ profile_location }}
</dd>
{% endif %}
{% if profile_fidonet_address and can_view_sensitive %}
<dt class="col-sm-3">{{ t('ui.user_profile.fidonet_address', {}, 'common') }}:</dt>
<dd class="col-sm-9">
<code>{{ profile_fidonet_address }}</code>
</dd>
{% endif %}
<dt class="col-sm-3">{{ t('ui.user_profile.member_since', {}, 'common') }}:</dt>
<dd class="col-sm-9">{{ profile_created_at|date('F j, Y') }}</dd>
{% if profile_last_login and can_view_sensitive %}
<dt class="col-sm-3">{{ t('ui.user_profile.last_seen', {}, 'common') }}:</dt>
<dd class="col-sm-9">{{ profile_last_login|date('F j, Y g:i A') }}</dd>
{% endif %}
<dt class="col-sm-3">{{ t('ui.user_profile.role', {}, 'common') }}:</dt>
<dd class="col-sm-9">
{% if profile_is_admin %}
<span class="badge bg-danger">{{ t('ui.user_profile.role_administrator', {}, 'common') }}</span>
{% else %}
<span class="badge bg-primary">{{ t('ui.user_profile.role_user', {}, 'common') }}</span>
{% endif %}
</dd>
</dl>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">{{ t('ui.user_profile.about_me', {}, 'common') }}</h5>
</div>
<div class="card-body">
{% if profile_about_me_html %}
<div class="about-me-rendered">{{ profile_about_me_html|raw }}</div>
{% else %}
<p class="text-muted mb-0"><em>{{ t('ui.user_profile.about_me_empty', {}, 'common') }}</em></p>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
{% if credits_enabled and can_view_sensitive %}
<div class="card">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.user_profile.credits', {}, 'common') }}</h6>
</div>
<div class="card-body">
<div class="text-center">
<h3 class="mb-0">{{ credits_symbol }}{{ credit_balance }}</h3>
<small class="text-muted">{{ t('ui.user_profile.current_balance', {}, 'common') }}</small>
</div>
</div>
</div>
{% endif %}
{% if credits_enabled and not is_own_profile %}
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.user_profile.send_credits', {}, 'common') }}</h6>
</div>
<div class="card-body">
<div id="sendCreditsAlert"></div>
<form id="sendCreditsForm">
<div class="mb-3">
<label for="creditAmount" class="form-label">{{ t('ui.user_profile.amount', {}, 'common') }}</label>
<input type="number" class="form-control" id="creditAmount" name="amount" min="1" max="200" required>
<small class="text-muted">{{ t('ui.user_profile.send_credits_limit_fee', {'fee_percent': (transfer_fee_percent * 100)|round(1)}, 'common') }}</small>
</div>
<div class="mb-3">
<label for="creditMessage" class="form-label">{{ t('ui.user_profile.message_optional', {}, 'common') }}</label>
<input type="text" class="form-control" id="creditMessage" name="message" maxlength="100" placeholder="{{ t('ui.user_profile.message_optional_placeholder', {}, 'common') }}">
</div>
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-paper-plane me-2"></i>{{ t('ui.user_profile.send_credits', {}, 'common') }}
</button>
</form>
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
{{ t('ui.user_profile.send_credits_fee_note', {'fee_percent': (transfer_fee_percent * 100)|round(1)}, 'common') }}
</small>
</div>
</div>
</div>
{% endif %}
{% if can_view_sensitive %}
<div class="card {% if credits_enabled %}mt-3{% endif %}">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.user_profile.activity_summary', {}, 'common') }}</h6>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-8">{{ t('ui.user_profile.netmail_sent', {}, 'common') }}:</dt>
<dd class="col-4"><span id="netmailCount">-</span></dd>
<dt class="col-8">{{ t('ui.user_profile.echomail_posted', {}, 'common') }}:</dt>
<dd class="col-4"><span id="echomailCount">-</span></dd>
<dt class="col-8">{{ t('ui.user_profile.total_messages', {}, 'common') }}:</dt>
<dd class="col-4"><strong><span id="totalCount">-</span></strong></dd>
</dl>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body text-center text-muted">
<i class="fas fa-lock fa-2x mb-2"></i>
<p class="mb-0">{{ t('ui.user_profile.additional_info_private', {}, 'common') }}</p>
</div>
</div>
{% endif %}
</div>
</div>
{% if viewer_is_admin %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<ul class="nav nav-tabs card-header-tabs" id="adminTabList" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-activity" data-bs-toggle="tab" data-bs-target="#pane-activity" type="button" role="tab">
<i class="fas fa-list-alt me-1"></i>
{{ t('ui.user_profile.tab.activity_log', {}, 'common') }}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-transactions" data-bs-toggle="tab" data-bs-target="#pane-transactions" type="button" role="tab">
<i class="fas fa-receipt me-1"></i>
{{ t('ui.user_profile.tab.transaction_history', {}, 'common') }}
</button>
</li>
</ul>
<span class="badge bg-secondary ms-2">{{ t('ui.user_profile.admin_view', {}, 'common') }}</span>
</div>
<div class="tab-content">
{# ?? Transaction History ????????????????????????????????????? #}
<div class="tab-pane fade" id="pane-transactions" role="tabpanel">
{% if credits_enabled %}
{% if transactions|length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width: 15%">{{ t('ui.user_profile.date', {}, 'common') }}</th>
<th style="width: 15%">{{ t('ui.user_profile.type', {}, 'common') }}</th>
<th>{{ t('ui.user_profile.description', {}, 'common') }}</th>
<th style="width: 12%" class="text-end">{{ t('ui.user_profile.amount', {}, 'common') }}</th>
<th style="width: 12%" class="text-end">{{ t('ui.user_profile.balance', {}, 'common') }}</th>
</tr>
</thead>
<tbody id="transactionTableBody">
{% for tx in transactions %}
<tr data-transaction-id="{{ tx.id }}">
<td>
<small>{{ tx.created_at|date('m/d/Y') }}</small><br>
<small class="text-muted">{{ tx.created_at|date('g:i A') }}</small>
</td>
<td>
<span class="badge bg-{% if tx.transaction_type == 'daily_login' %}info{% elseif tx.transaction_type == 'system_reward' %}success{% elseif tx.transaction_type == 'payment' %}primary{% elseif tx.transaction_type == 'admin_adjustment' %}warning{% else %}secondary{% endif %}">
{% if tx.transaction_type == 'daily_login' %}
{{ t('ui.user_profile.transaction_type.daily_login', {}, 'common') }}
{% elseif tx.transaction_type == 'system_reward' %}
{{ t('ui.user_profile.transaction_type.system_reward', {}, 'common') }}
{% elseif tx.transaction_type == 'payment' %}
{{ t('ui.user_profile.transaction_type.payment', {}, 'common') }}
{% elseif tx.transaction_type == 'admin_adjustment' %}
{{ t('ui.user_profile.transaction_type.admin_adjustment', {}, 'common') }}
{% else %}
{{ t('ui.user_profile.transaction_type.unknown', {}, 'common') }}
{% endif %}
</span>
</td>
<td>{{ tx.description }}</td>
<td class="text-end {% if tx.amount > 0 %}text-success{% else %}text-danger{% endif %}">
<strong>{{ tx.amount > 0 ? '+' : '' }}{{ credits_symbol }}{{ tx.amount }}</strong>
</td>
<td class="text-end">{{ credits_symbol }}{{ tx.balance_after }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer bg-light" id="loadMoreSection">
<button type="button" class="btn btn-sm btn-outline-secondary w-100" id="loadMoreBtn" onclick="loadMoreTransactions()">
<i class="fas fa-chevron-down me-2"></i>
{{ t('ui.user_profile.load_more_transactions', {}, 'common') }}
</button>
</div>
{% else %}
<div class="p-3 text-center text-muted">
<i class="fas fa-receipt fa-2x mb-2"></i>
<p class="mb-0">{{ t('ui.user_profile.no_transactions_yet', {}, 'common') }}</p>
</div>
{% endif %}
{% else %}
<div class="p-3 text-center text-muted">
<i class="fas fa-ban fa-2x mb-2"></i>
<p class="mb-0">{{ t('errors.credits.feature_disabled', {}, 'errors') }}</p>
</div>
{% endif %}
</div>
{# ?? Activity Log ???????????????????????????????????????????? #}
<div class="tab-pane fade show active" id="pane-activity" role="tabpanel">
{% if activity_log|length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width: 15%">{{ t('ui.user_profile.activity_log.date', {}, 'common') }}</th>
<th style="width: 13%">{{ t('ui.user_profile.activity_log.category', {}, 'common') }}</th>
<th style="width: 20%">{{ t('ui.user_profile.activity_log.activity', {}, 'common') }}</th>
<th>{{ t('ui.user_profile.activity_log.detail', {}, 'common') }}</th>
</tr>
</thead>
<tbody id="activityTableBody">
{% for entry in activity_log %}
<tr>
<td>
<small class="activity-date" data-utc="{{ entry.created_at }}"></small>
</td>
<td>
<span class="badge bg-{% if entry.category == 'message' %}primary{% elseif entry.category == 'file' %}success{% elseif entry.category == 'door' %}info{% elseif entry.category == 'chat' %}warning{% elseif entry.category == 'auth' %}danger{% else %}secondary{% endif %}">
{{ entry.category }}
</span>
</td>
<td><small>{{ entry.activity }}</small></td>
<td><small class="text-muted">{{ entry.object_name|default('') }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer bg-light" id="loadMoreActivitySection">
<button type="button" class="btn btn-sm btn-outline-secondary w-100" id="loadMoreActivityBtn" onclick="loadMoreActivity()">
<i class="fas fa-chevron-down me-2"></i>
{{ t('ui.user_profile.activity_log.load_more', {}, 'common') }}
</button>
</div>
{% else %}
<div class="p-3 text-center text-muted">
<i class="fas fa-list-alt fa-2x mb-2"></i>
<p class="mb-0">{{ t('ui.user_profile.activity_log.no_activity', {}, 'common') }}</p>
</div>
{% endif %}
</div>
</div>{# end tab-content #}
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
let currentOffset = 10; // We already loaded 10 transactions
let activityOffset = 25; // We already loaded 25 activity entries
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.activity-date[data-utc]').forEach(function(el) {
el.textContent = formatFullDate(el.getAttribute('data-utc'));
});
});
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
$(document).ready(function() {
{% if can_view_sensitive %}
loadActivityStats();
{% endif %}
{% if credits_enabled and not is_own_profile %}
$('#sendCreditsForm').on('submit', function(e) {
e.preventDefault();
sendCredits();
});
{% endif %}
});
function loadActivityStats() {
$.get('/api/user/stats/{{ profile_user_id }}')
.done(function(data) {
$('#netmailCount').text(data.netmail_count || 0);
$('#echomailCount').text(data.echomail_count || 0);
$('#totalCount').text((data.netmail_count || 0) + (data.echomail_count || 0));
})
.fail(function() {
$('#netmailCount, #echomailCount, #totalCount').text('?');
});
}
function sendCredits() {
const amount = parseInt($('#creditAmount').val());
const message = $('#creditMessage').val();
if (amount < 1 || amount > 200) {
showSendCreditsAlert(uiT('errors.credits.send.invalid_amount', 'Amount must be between 1 and 200 credits'), 'danger');
return;
}
const btn = $('#sendCreditsForm button[type=submit]');
const originalHtml = btn.html();
btn.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.user_profile.sending', 'Sending...')}`);
$.ajax({
url: '/api/credits/send',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
recipient_id: {{ profile_user_id }},
amount: amount,
message: message
}),
success: function(response) {
if (response.success) {
const fallbackMessage = uiT(
'ui.user_profile.send_credits_success',
'Successfully sent {{ credits_symbol }}{amount} to {{ profile_username }}! (Fee: {{ credits_symbol }}{fee}, They received: {{ credits_symbol }}{received})',
{
symbol: '{{ credits_symbol }}',
username: '{{ profile_username|e('js') }}',
amount: amount,
fee: response.fee,
received: response.amount_received
}
);
const successMessage = window.getApiMessage
? window.getApiMessage(response, fallbackMessage)
: fallbackMessage;
showSendCreditsAlert(successMessage, 'success');
$('#sendCreditsForm')[0].reset();
} else {
const err = window.getApiErrorMessage
? window.getApiErrorMessage(response, uiT('errors.credits.send.failed', 'Failed to send credits'))
: uiT('errors.credits.send.failed', 'Failed to send credits');
showSendCreditsAlert(err, 'danger');
}
},
error: function(xhr) {
const error = window.getApiErrorMessage
? window.getApiErrorMessage(xhr.responseJSON, uiT('errors.credits.send.failed', 'Failed to send credits'))
: uiT('errors.credits.send.failed', 'Failed to send credits');
showSendCreditsAlert(error, 'danger');
},
complete: function() {
btn.prop('disabled', false).html(originalHtml);
}
});
}
function showSendCreditsAlert(message, type) {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${escapeHtml(message)}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
$('#sendCreditsAlert').html(alertHtml);
if (type === 'success') {
setTimeout(function() {
$('#sendCreditsAlert').fadeOut(function() {
$(this).html('').show();
});
}, 5000);
}
}
function loadMoreTransactions() {
const btn = $('#loadMoreBtn');
const originalHtml = btn.html();
btn.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.common.loading', 'Loading...')}`);
$.get('/api/user/transactions/{{ profile_user_id }}?offset=' + currentOffset + '&limit=10')
.done(function(data) {
if (data.transactions && data.transactions.length > 0) {
const tbody = $('#transactionTableBody');
data.transactions.forEach(function(tx) {
const fullDate = typeof formatFullDate === 'function'
? formatFullDate(tx.created_at)
: tx.created_at;
let badgeClass = 'secondary';
if (tx.transaction_type === 'daily_login') badgeClass = 'info';
else if (tx.transaction_type === 'system_reward') badgeClass = 'success';
else if (tx.transaction_type === 'payment') badgeClass = 'primary';
else if (tx.transaction_type === 'admin_adjustment') badgeClass = 'warning';
const typeLabel = getTransactionTypeLabel(tx.transaction_type);
const amountClass = tx.amount > 0 ? 'text-success' : 'text-danger';
const amountSign = tx.amount > 0 ? '+' : '';
const row = `<tr data-transaction-id="${tx.id}">
<td>
<small>${escapeHtml(fullDate)}</small>
</td>
<td>
<span class="badge bg-${badgeClass}">${typeLabel}</span>
</td>
<td>${escapeHtml(tx.description)}</td>
<td class="text-end ${amountClass}">
<strong>${amountSign}{{ credits_symbol }}${tx.amount}</strong>
</td>
<td class="text-end">{{ credits_symbol }}${tx.balance_after}</td>
</tr>`;
tbody.append(row);
});
currentOffset += data.transactions.length;
// Hide button if we loaded less than requested (no more transactions)
if (data.transactions.length < 10) {
$('#loadMoreSection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.no_more_transactions', 'No more transactions')}</small></div>`);
}
} else {
$('#loadMoreSection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.no_more_transactions', 'No more transactions')}</small></div>`);
}
})
.fail(function() {
alert(uiT('ui.user_profile.load_transactions_failed', 'Failed to load transactions'));
})
.always(function() {
btn.prop('disabled', false).html(originalHtml);
});
}
function getTransactionTypeLabel(type) {
const map = {
daily_login: uiT('ui.user_profile.transaction_type.daily_login', 'Daily Login'),
system_reward: uiT('ui.user_profile.transaction_type.system_reward', 'System Reward'),
payment: uiT('ui.user_profile.transaction_type.payment', 'Payment'),
admin_adjustment: uiT('ui.user_profile.transaction_type.admin_adjustment', 'Admin Adjustment')
};
return map[type] || uiT('ui.user_profile.transaction_type.unknown', 'Unknown');
}
function getCategoryBadgeClass(category) {
const map = { message: 'primary', file: 'success', door: 'info', chat: 'warning', auth: 'danger' };
return map[category] || 'secondary';
}
function loadMoreActivity() {
const btn = $('#loadMoreActivityBtn');
const originalHtml = btn.html();
btn.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.common.loading', 'Loading...')}`);
$.get(`/api/user/activity/{{ profile_user_id }}?offset=${activityOffset}&limit=25`)
.done(function(data) {
if (data.activity && data.activity.length > 0) {
const tbody = $('#activityTableBody');
data.activity.forEach(function(entry) {
const badgeClass = getCategoryBadgeClass(entry.category);
const row = `<tr>
<td>
<small>${escapeHtml(formatFullDate(entry.created_at))}</small>
</td>
<td><span class="badge bg-${badgeClass}">${escapeHtml(entry.category)}</span></td>
<td><small>${escapeHtml(entry.activity)}</small></td>
<td><small class="text-muted">${escapeHtml(entry.object_name || '')}</small></td>
</tr>`;
tbody.append(row);
});
activityOffset += data.activity.length;
if (data.activity.length < 25) {
$('#loadMoreActivitySection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.activity_log.no_more', 'No more activity')}</small></div>`);
}
} else {
$('#loadMoreActivitySection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.activity_log.no_more', 'No more activity')}</small></div>`);
}
})
.fail(function() {
alert(uiT('ui.user_profile.activity_log.load_failed', 'Failed to load activity log'));
})
.always(function() {
btn.prop('disabled', false).html(originalHtml);
});
}
</script>
{% endblock %}
|