Compare commits

...

6 Commits

Author SHA1 Message Date
xds
fb60c69846 fix tx isdone checkbox text size 2026-03-10 15:54:34 +03:00
xds
a5f4d75306 fix tx isdone checkbox text size 2026-03-10 15:41:46 +03:00
xds
fbb3909531 fix tx isdone checkbox text size 2026-03-10 15:37:36 +03:00
xds
853d5111e2 fix tx isdone checkbox text size 2026-03-10 15:33:50 +03:00
xds
70f4107840 Merge remote-tracking branch 'origin/main' 2026-03-10 15:09:05 +03:00
xds
a0b4a4bcf3 fix tx isdone checkbox text size 2026-03-10 15:08:24 +03:00
9 changed files with 131 additions and 4 deletions

1
.env
View File

@@ -1,3 +1,4 @@
# Базовые настройки для всех режимов
VITE_APP_NAME=Space
VITE_API_TIMEOUT=5000
VITE_GOOGLE_CLIENT_ID=112729998586-q39qsptu67lqeej0356m01e1ghptuajk.apps.googleusercontent.com

View File

@@ -10,6 +10,7 @@
<link rel="apple-touch-icon" href="/src/assets/1024.png">
<link rel="stylesheet" href="/src/assets/main.css"/>
<script src="https://telegram.org/js/telegram-web-app.js?59"></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<title>Luminic Space</title>
</head>
<body>

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {Divider, ToggleSwitch} from "primevue";
import {Divider, ToggleSwitch, Button} from "primevue";
import {spaceService} from "@/services/space-service";
import {SettingType} from "@/models/enums";
import {useUserStore} from "@/stores/userStore";
const userStore = useUserStore()
const spacePeriodStarts = ref(10)
const spaceSubPeriodStarts = ref(25)
@@ -87,6 +89,31 @@ const settingChanged = async (setting: SettingType) => {
}
declare global {
interface Window {
google: any;
}
}
const linkGoogleDrive = () => {
if (typeof window.google === 'undefined') {
console.error('Google client not loaded')
return
}
const client = window.google.accounts.oauth2.initCodeClient({
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
scope: 'https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.readonly',
ux_mode: 'popup',
callback: async (response: any) => {
if (response.code) {
await userStore.linkGoogleDrive(response.code)
}
},
});
client.requestCode();
}
onMounted(async () => {
await fetchData()
})
@@ -143,6 +170,25 @@ onMounted(async () => {
</div>
</div>
</div>
<div class="flex backup flex-col !w-full mt-2">
<span class="text-sm !pl-2">Storage & Backup</span>
<div class="flex card flex-col !w-full justify-between p-2">
<div class="flex flex-row !w-full justify-between items-center">
<div class="flex flex-col">
<span>Google Drive</span>
<span class="text-xs text-gray-500" v-if="userStore.user?.isGoogleDriveConnected">Connected</span>
<span class="text-xs text-gray-500" v-else>Not connected</span>
</div>
<Button
:label="userStore.user?.isGoogleDriveConnected? 'Reconnect' : 'Connect'"
:icon="userStore.user?.isGoogleDriveConnected ? 'pi pi-refresh' : 'pi pi-google'"
:severity="userStore.user?.isGoogleDriveConnected ? 'secondary' : 'primary'"
size="small"
@click="linkGoogleDrive"
/>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -173,6 +173,31 @@ const fetchData = async (fetchPlanned: boolean = true, fetchInstant: boolean = t
}
const downloadTransactions = async () => {
if (!spaceStore.selectedSpaceId) return
try {
await transactionService.exportTransactions(spaceStore.selectedSpaceId, {
query: searchQuery.value || null,
categoriesIds: selectedCategoryIds.value.length > 0 ? selectedCategoryIds.value : null,
dateFrom: filterDateFrom.value ? toDateOnly(filterDateFrom.value) : null,
dateTo: filterDateTo.value ? toDateOnly(filterDateTo.value) : null,
} as TransactionFilters)
toast.add({
severity: 'success',
summary: 'Success',
detail: 'Transactions exported successfully',
life: 3000,
})
} catch (e) {
toast.add({
severity: 'error',
summary: 'Failed to export transactions.',
detail: String(e),
life: 3000,
})
}
}
onMounted(async () => {
await fetchCategories()
await fetchData()
@@ -194,6 +219,7 @@ onMounted(async () => {
<InputIcon class="pi pi-search"> </InputIcon>
<InputText v-model="searchQuery" placeholder="Search" class="!w-full !bg-white !text-left" />
</IconField>
<Button icon="pi pi-download" @click="downloadTransactions" text rounded aria-label="Download" />
<Button icon="pi pi-filter" @click="isFilterSheetVisible = true" text rounded aria-label="Filter" />
</div>
<div class="flex flex-row justify-between">

1
src/env.d.ts vendored
View File

@@ -5,6 +5,7 @@ interface ImportMetaEnv {
readonly VITE_ENABLE_DEVTOOLS: boolean
readonly VITE_APP_NAME: string
readonly VITE_API_TIMEOUT: number
readonly VITE_GOOGLE_CLIENT_ID: string
// добавь сюда свои переменные, если есть
// readonly VITE_APP_TITLE: string
}

View File

@@ -5,4 +5,5 @@ export interface User {
tgId: number
tdUserName: string;
roles: string[];
isGoogleDriveConnected?: boolean;
}

View File

@@ -80,6 +80,36 @@ async function deleteTransaction(spaceId: number, txId: number): Promise<void> {
}
}
export const TransactionService = {
getTransactions, getTransaction, createTransaction, updateTransaction, deleteTransaction,
async function exportTransactions(spaceId: number, filters: TransactionFilters): Promise<void> {
try {
let response = await api.post(`/spaces/${spaceId}/transactions/_export`, filters, {
responseType: 'blob'
});
const contentDisposition = response.headers['content-disposition'];
let fileName = `transactions_${spaceId}_${toDateOnly(new Date())}.csv`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename\*?=['"]?(?:UTF-8'')?([^; '"]+)['"]?/i);
if (fileNameMatch && fileNameMatch[1]) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
const url = window.URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
throw error;
}
}
export const TransactionService = {
getTransactions, getTransaction, createTransaction, updateTransaction, deleteTransaction, exportTransactions
}

View File

@@ -141,6 +141,24 @@ export const useUserStore = defineStore('user', () => {
}
}
return {user, loadingUser, fetchUserProfile, tgLogin, tgLoginData, register, isAuthorized};
async function linkGoogleDrive(code: string) {
try {
await apiClient.post('/auth/me/google-drive', {
code: code
});
toast.add({severity: 'success', summary: 'Google Drive connected', detail: 'Successful!', life: 3000})
await fetchUserProfile(); // refresh user info to get updated isGoogleDriveLinked
} catch (error: any) {
console.error(error);
toast.add({
severity: 'error',
summary: 'Connection error',
detail: error.response?.data?.message || 'Error occurred while linking Google Drive',
life: 3000
})
}
}
return {user, loadingUser, fetchUserProfile, tgLogin, tgLoginData, register, linkGoogleDrive, isAuthorized};
});

View File

@@ -18,5 +18,8 @@ export default defineConfig(async ({mode}) => {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
optimizeDeps: {
exclude: ['chart.js'],
},
}
})