last work on invoice functionality

pull/17/head
Suvodip 2025-03-28 20:26:50 +05:30
parent 1ed908b12e
commit c927fd6087
3 changed files with 196 additions and 60 deletions

View File

@ -26,6 +26,7 @@ interface AuthResponse {
record: UserRecord; record: UserRecord;
} }
const LoginPage = () => { const LoginPage = () => {
const [email, setEmail] = useState('suvodip@siliconpin.com'); const [email, setEmail] = useState('suvodip@siliconpin.com');
const [password, setPassword] = useState('Simple2pass'); const [password, setPassword] = useState('Simple2pass');
@ -35,18 +36,30 @@ const LoginPage = () => {
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com"); const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
interface AuthResponse {
token: string;
record: {
query: string;
id: string;
email: string;
name: string;
avatar: string;
};
}
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setStatus({ message: '', isError: false }); setStatus({ message: '', isError: false });
try { try {
const authData = await pb.collection("users").authWithPassword(email, password); const authData = await pb.collection("users").authWithPassword(email, password);
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
const authResponse: AuthResponse = { const authResponse: AuthResponse = {
token: authData.token, token: authData.token,
record: { record: {
query: 'new', query: 'new',
id: authData.record.id, id: authData.record.id,
email: authData.record.email, email: authData.record.email,
name: authData.record.name || '', name: authData.record.name || '',
@ -54,8 +67,8 @@ const LoginPage = () => {
} }
}; };
await syncSessionWithBackend(authResponse); await syncSessionWithBackend(authResponse, avatarUrl);
// window.location.href = '/profile'; window.location.href = '/profile';
} catch (error) { } catch (error) {
console.error("Login failed:", error); console.error("Login failed:", error);
setStatus({ setStatus({
@ -77,20 +90,21 @@ const LoginPage = () => {
if (!authData?.record) { if (!authData?.record) {
throw new Error("No user record found"); throw new Error("No user record found");
} }
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
const authResponse: AuthResponse = { const authResponse: AuthResponse = {
token: authData.token, token: authData.token,
record: { record: {
query: 'new', query: 'new',
id: authData.record.id, id: authData.record.id,
email: authData.record.email || '', email: authData.record.email || '',
name: authData.record.name || '', name: authData.record.name || '',
avatar: authData.record.avatar || '' avatar: authData.record.avatar || ''
} }
}; };
await syncSessionWithBackend(authResponse); await syncSessionWithBackend(authResponse, avatarUrl);
// window.location.href = '/profile'; window.location.href = '/profile';
} catch (error) { } catch (error) {
console.error(`${provider} Login failed:`, error); console.error(`${provider} Login failed:`, error);
setStatus({ setStatus({
@ -101,33 +115,35 @@ const LoginPage = () => {
setIsLoading(false); setIsLoading(false);
} }
}; };
const syncSessionWithBackend = async (authData: AuthResponse) => { const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
try { try {
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', { const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
method: 'POST', method: 'POST',
credentials: 'include', // Crucial for cookies credentials: 'include', // Important for cookies
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
accessToken: authData.token, query: 'new',
email: authData.record.email, accessToken: authData.token,
name: authData.record.name, email: authData.record.email,
avatar: authData.record.avatar name: authData.record.name,
? pb.files.getUrl(authData.record, authData.record.avatar) avatar: avatarUrl,
: '', isAuthenticated: true,
isAuthenticated: true, id: authData.record.id
id: authData.record.id })
}) });
});
if (!response.ok) {
if (!response.ok) throw new Error('Failed to sync session'); throw new Error('Failed to sync session');
}
const data = await response.json();
console.log('Session synced:', data); const data = await response.json();
} catch (error) { console.log('Session synced with backend:', data);
console.error('Error syncing session:', error); } catch (error) {
} console.error('Error syncing session:', error);
}; throw error; // Re-throw the error if you want calling functions to handle it
}
};
return ( return (
<div className="min-h-screen flex items-center justify-center p-4"> <div className="min-h-screen flex items-center justify-center p-4">

View File

@ -0,0 +1,52 @@
import { useState, useRef } from 'react';
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { X } from "lucide-react"; // Import an icon for the remove button
export function AvatarUpload() {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setSelectedFile(e.target.files[0]);
}
};
const handleRemoveFile = () => {
setSelectedFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = ''; // Reset file input
}
};
return (
<div className="space-y-4">
{!selectedFile ? (
<div className="space-y-2">
<div className="flex items-center gap-4">
<Label
htmlFor="avatar"
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colorsfocus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
Change Avatar
</Label>
<Input type="file" id="avatar" ref={fileInputRef} accept="image/jpeg,image/png,image/gif" className="hidden" onChange={handleFileChange}/>
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>Browse</Button>
</div>
<p className="text-xs text-muted-foreground">
JPG, GIF or PNG. 1MB max.
</p>
</div>
) : (
<div className="flex items-center justify-between p-3 space-x-2">
<div className="truncate max-w-[200px]">
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
</div>
<Button size="sm" className="text-xs p-1 h-fit">Update</Button>
<Button size="sm" onClick={handleRemoveFile} className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit">Remove</Button>
</div>
)}
</div>
);
}
export default AvatarUpload;

View File

@ -7,6 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from ".
import { Separator } from "./ui/separator"; import { Separator } from "./ui/separator";
import { Textarea } from "./ui/textarea"; import { Textarea } from "./ui/textarea";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import UpdateAvatar from './UpdateAvatar';
interface SessionData { interface SessionData {
[key: string]: any; [key: string]: any;
@ -21,6 +22,7 @@ interface UserData {
export default function ProfilePage() { export default function ProfilePage() {
const [userData, setUserData] = useState<UserData | null>(null); const [userData, setUserData] = useState<UserData | null>(null);
const [invoiceList, setInvoiceList] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@ -45,8 +47,36 @@ export default function ProfilePage() {
throw error; throw error;
} }
}; };
const getInvoiceListData = async () => {
try {
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
method: 'GET',
credentials: 'include', // Crucial for cookies
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Session fetch failed');
}
setInvoiceList(data.data); // Fix: Use `data.data` instead of `data`
return data.data; // Fix: `session_data` does not exist in response
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
};
fetchSessionData(); fetchSessionData();
getInvoiceListData();
}, []); }, []);
if (error) { if (error) {
@ -58,12 +88,10 @@ export default function ProfilePage() {
} }
return ( return (
<div className="space-y-6 container mx-auto"> <div className="space-y-6 container mx-auto">
<div> {/* <div>
<h3 className="text-lg font-medium">Profile</h3> <h3 className="text-lg font-medium">Profile</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Update your profile settings.</p>
Update your profile settings. </div> */}
</p>
</div>
<Separator /> <Separator />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<div className="flex-1 lg:max-w-2xl"> <div className="flex-1 lg:max-w-2xl">
@ -80,14 +108,8 @@ export default function ProfilePage() {
<AvatarImage src={userData.session_data?.user_avatar} /> <AvatarImage src={userData.session_data?.user_avatar} />
<AvatarFallback>JP</AvatarFallback> <AvatarFallback>JP</AvatarFallback>
</Avatar> </Avatar>
<div className="space-y-1"> <UpdateAvatar />
<Button size="sm">
Change avatar
</Button>
<p className="text-xs text-muted-foreground">
JPG, GIF or PNG. 1MB max.
</p>
</div>
</div> </div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
@ -106,7 +128,7 @@ export default function ProfilePage() {
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} /> <Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
</div> </div>
<div className="space-y-2"> {/* <div className="space-y-2">
<Label htmlFor="bio">Bio</Label> <Label htmlFor="bio">Bio</Label>
<Textarea <Textarea
id="bio" id="bio"
@ -114,11 +136,58 @@ export default function ProfilePage() {
className="resize-none" className="resize-none"
rows={5} rows={5}
/> />
</div> </div> */}
</CardContent> </CardContent>
</Card> </Card>
<Card className="mt-6"> <Card className="mt-6">
<CardHeader>
<CardTitle>Billing Information</CardTitle>
<CardDescription>
View your billing history.
</CardDescription>
{/* <CardContent> */}
<table className="w-full">
<thead>
<tr>
<th className="text-left">Invoice ID</th>
<th className="text-left">Date</th>
<th className="text-left">Description</th>
<th className="text-right">Amount</th>
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{/* {
invoiceList.map((invoice) => (
<tr key={invoice.id}>
<td>{invoice.invoice_id}</td>
<td>{invoice.date}</td>
<td>{invoice.description}</td>
<td className="text-right">{invoice.amount}</td>
<td className="text-center"><a href="">Print</a></td>
</tr>
))
} */}
<tr>
<td>dcdicdicib</td>
<td>2023-10-01</td>
<td>Subscription Fee</td>
<td className="text-right">$10.00</td>
<td className="text-center"><a href="">Print</a></td>
</tr>
<tr>
<td>dcibcdici</td>
<td>2023-09-01</td>
<td>Subscription Fee</td>
<td className="text-right">$10.00</td>
<td className="text-center"><a href="" >Print</a></td>
</tr>
</tbody>
</table>
{/* </CardContent> */}
</CardHeader>
</Card>
{/* <Card className="mt-6">
<CardHeader> <CardHeader>
<CardTitle>Preferences</CardTitle> <CardTitle>Preferences</CardTitle>
<CardDescription> <CardDescription>
@ -156,7 +225,7 @@ export default function ProfilePage() {
</Select> </Select>
</div> </div>
</CardContent> </CardContent>
</Card> </Card> */}
</div> </div>
<div className="flex-1 space-y-6 lg:max-w-md"> <div className="flex-1 space-y-6 lg:max-w-md">
@ -183,8 +252,7 @@ export default function ProfilePage() {
<Button className="mt-4">Update password</Button> <Button className="mt-4">Update password</Button>
</CardContent> </CardContent>
</Card> </Card>
<Card className="mt-6">
<Card>
<CardHeader> <CardHeader>
<CardTitle>Danger Zone</CardTitle> <CardTitle>Danger Zone</CardTitle>
<CardDescription> <CardDescription>
@ -193,7 +261,7 @@ export default function ProfilePage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<Button >Delete account</Button> <Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
<Button variant="outline">Export data</Button> <Button variant="outline">Export data</Button>
</div> </div>
</CardContent> </CardContent>