Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
This commit is contained in:
432
reports-app/frontend/src/views/TelegramView.vue
Normal file
432
reports-app/frontend/src/views/TelegramView.vue
Normal file
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Dashboard Header -->
|
||||
<DashboardHeader @menu-toggle="handleMenuToggle" />
|
||||
|
||||
<!-- Hamburger Menu -->
|
||||
<HamburgerMenu :is-open="menuOpen" @close="handleMenuClose" />
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="app-container">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Telegram Bot</h1>
|
||||
<p class="page-subtitle">Conectează-ți contul pentru acces rapid din Telegram</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Se generează codul...</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Card -->
|
||||
<div v-else class="telegram-card">
|
||||
|
||||
<!-- Generate Button -->
|
||||
<div class="generate-section">
|
||||
<button
|
||||
@click="generateCode"
|
||||
:disabled="loading"
|
||||
class="generate-btn"
|
||||
>
|
||||
{{ loading ? 'Se generează...' : 'Generează Cod' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Code Display & Actions -->
|
||||
<div v-if="linkingCode" class="code-section">
|
||||
|
||||
<!-- Code Display -->
|
||||
<div class="code-display">
|
||||
<div class="code-header">
|
||||
<span class="code-label">Cod</span>
|
||||
<span class="code-timer">{{ formatTime(timeRemaining) }}</span>
|
||||
</div>
|
||||
<div class="code-value">{{ linkingCode }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<a
|
||||
:href="telegramDeepLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="action-btn primary-action-btn"
|
||||
>
|
||||
Deschide Telegram
|
||||
</a>
|
||||
<Button
|
||||
:label="showQR ? 'Ascunde QR' : 'Arată QR'"
|
||||
@click="showQR = !showQR"
|
||||
class="action-btn"
|
||||
outlined
|
||||
/>
|
||||
<Button
|
||||
label="Copiază Cod"
|
||||
@click="copyCode"
|
||||
class="action-btn"
|
||||
outlined
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Display -->
|
||||
<div v-if="showQR" class="qr-section">
|
||||
<QRCodeVue :value="telegramDeepLink" :size="200" level="H" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Toast -->
|
||||
<Toast position="top-right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import DashboardHeader from '../components/layout/DashboardHeader.vue'
|
||||
import HamburgerMenu from '../components/layout/HamburgerMenu.vue'
|
||||
import Button from 'primevue/button'
|
||||
import Toast from 'primevue/toast'
|
||||
import QRCodeVue from 'qrcode.vue'
|
||||
import { apiService } from '../services/api'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
// State
|
||||
const menuOpen = ref(false)
|
||||
const linkingCode = ref('')
|
||||
const timeRemaining = ref(0)
|
||||
const loading = ref(false)
|
||||
const showQR = ref(false)
|
||||
|
||||
let countdownInterval = null
|
||||
|
||||
// Config
|
||||
const BOT_USERNAME = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || 'roa2web_bot'
|
||||
|
||||
// Computed
|
||||
const telegramDeepLink = computed(() => {
|
||||
if (!linkingCode.value) return ''
|
||||
return `https://t.me/${BOT_USERNAME}?start=${linkingCode.value}`
|
||||
})
|
||||
|
||||
// Methods
|
||||
const handleMenuToggle = (isOpen) => {
|
||||
menuOpen.value = isOpen
|
||||
}
|
||||
|
||||
const handleMenuClose = () => {
|
||||
menuOpen.value = false
|
||||
}
|
||||
|
||||
const generateCode = async () => {
|
||||
loading.value = true
|
||||
showQR.value = false
|
||||
|
||||
try {
|
||||
const response = await apiService.post('/telegram/auth/generate-code')
|
||||
linkingCode.value = response.data.linking_code
|
||||
timeRemaining.value = response.data.expires_in_minutes * 60
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Cod Generat',
|
||||
detail: 'Alege o metodă de conectare',
|
||||
life: 3000
|
||||
})
|
||||
|
||||
startCountdown()
|
||||
} catch (error) {
|
||||
console.error('Error generating code:', error)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.response?.data?.detail || 'Nu am putut genera codul',
|
||||
life: 5000
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startCountdown = () => {
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
|
||||
countdownInterval = setInterval(() => {
|
||||
if (timeRemaining.value > 0) {
|
||||
timeRemaining.value--
|
||||
} else {
|
||||
clearInterval(countdownInterval)
|
||||
linkingCode.value = ''
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Cod Expirat',
|
||||
detail: 'Generează un cod nou',
|
||||
life: 4000
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const copyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(linkingCode.value)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Copiat',
|
||||
detail: 'Cod copiat în clipboard',
|
||||
life: 2000
|
||||
})
|
||||
} catch (error) {
|
||||
const tempInput = document.createElement('input')
|
||||
tempInput.value = linkingCode.value
|
||||
document.body.appendChild(tempInput)
|
||||
tempInput.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(tempInput)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Copiat',
|
||||
life: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Layout - Consistent with Dashboard */
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
padding: var(--space-lg);
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.app-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.page-header {
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 var(--space-xs) 0;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-3xl);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Main Card */
|
||||
.telegram-card {
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: var(--space-xl);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Generate Section */
|
||||
.generate-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: var(--space-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
min-width: 200px;
|
||||
padding: 12px 24px;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.generate-btn:hover:not(:disabled) {
|
||||
background: #4f46e5;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.generate-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.generate-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Code Section */
|
||||
.code-section {
|
||||
margin-top: var(--space-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
/* Code Display */
|
||||
.code-display {
|
||||
background: linear-gradient(135deg, rgba(67, 97, 238, 0.08), rgba(67, 97, 238, 0.02));
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-xs);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.code-label {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.code-timer {
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-bold);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.code-value {
|
||||
font-size: 2rem;
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.3em;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.primary-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
padding: 11px 20px;
|
||||
background: var(--primary-500, #6366f1);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.primary-action-btn:hover {
|
||||
background: var(--primary-600, #4f46e5);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
/* QR Section */
|
||||
.qr-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--space-lg);
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.telegram-card {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.code-value {
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-btn,
|
||||
.primary-btn {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user