🚀 Hotfixes: inline PDF preview and instant profile update in UI

This commit is contained in:
Hamza-Ayed
2026-04-22 17:15:43 +03:00
parent 4c2fd7bba5
commit 7e0e271be2
3 changed files with 31 additions and 1 deletions

View File

@@ -18,7 +18,9 @@ import {
UploadedFile, UploadedFile,
UseInterceptors, UseInterceptors,
ParseUUIDPipe, ParseUUIDPipe,
Res,
} from '@nestjs/common'; } from '@nestjs/common';
import { Response } from 'express';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { InvoicesService } from './invoice.service'; import { InvoicesService } from './invoice.service';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
@@ -111,7 +113,25 @@ export class InvoicesController {
async getFile( async getFile(
@CurrentUser() user: any, @CurrentUser() user: any,
@Param('id', ParseUUIDPipe) id: string, @Param('id', ParseUUIDPipe) id: string,
@Res({ passthrough: true }) res: Response,
) { ) {
return this.invoicesService.getFile(user.tenantId, id); const streamableFile = await this.invoicesService.getFile(user.tenantId, id);
// We need to determine the content type to ensure it opens inline in the browser (iframe)
// We can fetch the invoice details first to get the extension.
const invoice = await this.invoicesService.findOne(user.tenantId, id);
let mimeType = 'application/pdf'; // Default fallback
if (invoice.original_file_path) {
const ext = invoice.original_file_path.split('.').pop()?.toLowerCase();
if (ext === 'jpg' || ext === 'jpeg') mimeType = 'image/jpeg';
else if (ext === 'png') mimeType = 'image/png';
}
res.set({
'Content-Type': mimeType,
'Content-Disposition': 'inline', // This forces the browser to display it instead of downloading
});
return streamableFile;
} }
} }

View File

@@ -21,6 +21,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
import { useAuthStore } from '../../store/authStore';
export const SettingsPage = () => { export const SettingsPage = () => {
const [activeTab, setActiveTab] = useState('profile'); const [activeTab, setActiveTab] = useState('profile');
@@ -37,6 +38,8 @@ export const SettingsPage = () => {
language: 'العربية' language: 'العربية'
}); });
const updateUser = useAuthStore((state) => state.updateUser);
useEffect(() => { useEffect(() => {
const fetchProfile = async () => { const fetchProfile = async () => {
try { try {
@@ -62,6 +65,7 @@ export const SettingsPage = () => {
setIsSaving(true); setIsSaving(true);
try { try {
await apiClient.post('/users/profile', formData); await apiClient.post('/users/profile', formData);
updateUser({ name: formData.name });
setShowSuccess(true); setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000); setTimeout(() => setShowSuccess(false), 3000);
} catch (err) { } catch (err) {

View File

@@ -20,6 +20,7 @@ interface AuthState {
isAuthenticated: boolean; isAuthenticated: boolean;
setAuth: (user: User, token: string) => void; setAuth: (user: User, token: string) => void;
clearAuth: () => void; clearAuth: () => void;
updateUser: (data: Partial<User>) => void;
} }
export const useAuthStore = create<AuthState>()( export const useAuthStore = create<AuthState>()(
@@ -35,6 +36,11 @@ export const useAuthStore = create<AuthState>()(
localStorage.removeItem('access_token'); localStorage.removeItem('access_token');
set({ user: null, isAuthenticated: false }); set({ user: null, isAuthenticated: false });
}, },
updateUser: (data) => {
set((state) => ({
user: state.user ? { ...state.user, ...data } : null
}));
},
}), }),
{ {
name: 'musadaq-auth-storage', name: 'musadaq-auth-storage',