feat: Implement admin user management features

- Added adminCreateUser endpoint and corresponding DTO for creating users.
- Implemented deleteUser and resetPassword functionalities for admin users.
- Introduced updateEmail functionality for admin users.
- Updated UsersService to handle user creation, deletion, password reset, and email updates.
- Modified UsersController to include new admin routes with appropriate role checks.
- Refactored frontend navigation to link to user management under profile.
- Created new profile tabs for user management and database management.
- Developed AnvandareClient component for user management, including user creation, deletion, role changes, and password resets.
- Added DatabsTab for managing product listings and merging duplicates.
- Enhanced MinProfilTab for user profile management with form handling.
This commit is contained in:
Nils-Johan Gynther
2026-04-18 14:49:02 +02:00
parent 00dc0d6c69
commit 537a4f8ab6
16 changed files with 1141 additions and 66 deletions
+56
View File
@@ -0,0 +1,56 @@
'use client';
import Link from 'next/link';
type Tab = { id: string; label: string };
const USER_TABS: Tab[] = [{ id: 'profil', label: 'Min profil' }];
const ADMIN_TABS: Tab[] = [
{ id: 'profil', label: 'Min profil' },
{ id: 'anvandare', label: '👥 Användare' },
{ id: 'databas', label: '🗄️ Databas' },
];
type Props = {
activeTab: string;
isAdmin: boolean;
};
export default function ProfileTabs({ activeTab, isAdmin }: Props) {
const tabs = isAdmin ? ADMIN_TABS : USER_TABS;
return (
<div
style={{
display: 'flex',
gap: '0.25rem',
borderBottom: '2px solid #e5e7eb',
marginBottom: '2rem',
}}
>
{tabs.map((tab) => {
const active = tab.id === activeTab;
return (
<Link
key={tab.id}
href={`/profil?tab=${tab.id}`}
style={{
padding: '0.6rem 1.2rem',
textDecoration: 'none',
fontWeight: active ? 600 : 400,
fontSize: '0.95rem',
color: active ? '#2563eb' : '#555',
borderBottom: active ? '2px solid #2563eb' : '2px solid transparent',
marginBottom: '-2px',
borderRadius: '4px 4px 0 0',
background: active ? '#eff6ff' : 'transparent',
transition: 'background 0.15s',
}}
>
{tab.label}
</Link>
);
})}
</div>
);
}
+28 -4
View File
@@ -1,16 +1,40 @@
import { auth } from '../../auth';
import Navigation from '../Navigation';
import ProfileClient from './ProfileClient';
import ProfileTabs from './ProfileTabs';
import MinProfilTab from './tabs/MinProfilTab';
export const metadata = { title: 'Min profil' };
export default function ProfilPage() {
type Props = {
searchParams: Promise<{ tab?: string }>;
};
export default async function ProfilPage({ searchParams }: Props) {
const { tab = 'profil' } = await searchParams;
const session = await auth();
const isAdmin = (session?.user as any)?.role === 'admin';
// DatabsTab och AnvandareTab laddas dynamiskt för att hålla page.tsx tunn
let TabContent: React.ComponentType;
if (tab === 'databas' && isAdmin) {
const { default: DatabsTab } = await import('./tabs/DatabsTab');
TabContent = DatabsTab;
} else if (tab === 'anvandare' && isAdmin) {
const { default: AnvandareTab } = await import('./tabs/AnvandareTab');
TabContent = AnvandareTab;
} else {
TabContent = MinProfilTab;
}
return (
<>
<Navigation />
<main style={{ padding: '1rem', maxWidth: '800px', margin: '0 auto' }}>
<main style={{ padding: '1rem', maxWidth: '1200px', margin: '0 auto' }}>
<h1 style={{ marginBottom: '1.5rem' }}>Min profil</h1>
<ProfileClient />
<ProfileTabs activeTab={tab === 'databas' || tab === 'anvandare' ? tab : 'profil'} isAdmin={isAdmin} />
<TabContent />
</main>
</>
);
}
@@ -0,0 +1,575 @@
'use client';
import { useState } from 'react';
interface User {
id: number;
username: string;
email: string;
role: string;
firstName?: string;
lastName?: string;
createdAt: string;
}
interface ResetResult {
to: string;
subject: string;
body: string;
temporaryPassword: string;
}
interface Props {
users: User[];
currentUserId: number;
}
export default function AnvandareClient({ users: initial, currentUserId }: Props) {
const [users, setUsers] = useState<User[]>(initial);
const [error, setError] = useState('');
const [showCreate, setShowCreate] = useState(false);
const [creating, setCreating] = useState(false);
const [createForm, setCreateForm] = useState({
username: '',
email: '',
password: '',
role: 'user',
});
const [createError, setCreateError] = useState('');
// Lösenordsåterställning modal
const [resetResult, setResetResult] = useState<ResetResult | null>(null);
const [copiedBody, setCopiedBody] = useState(false);
const [copiedPw, setCopiedPw] = useState(false);
// Inline e-postbyte
const [editingEmailId, setEditingEmailId] = useState<number | null>(null);
const [editingEmail, setEditingEmail] = useState('');
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
setCreating(true);
setCreateError('');
try {
const res = await fetch('/api/admin-users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(createForm),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? 'Kunde inte skapa användare');
setUsers((prev) => [...prev, data]);
setShowCreate(false);
setCreateForm({ username: '', email: '', password: '', role: 'user' });
} catch (err: any) {
setCreateError(err.message);
} finally {
setCreating(false);
}
}
async function handleDelete(id: number) {
if (!confirm('Är du säker på att du vill ta bort användaren?')) return;
setError('');
const res = await fetch(`/api/admin-users/${id}`, { method: 'DELETE' });
if (res.ok) {
setUsers((prev) => prev.filter((u) => u.id !== id));
} else {
const data = await res.json().catch(() => ({}));
setError(data.message ?? 'Kunde inte ta bort användaren');
}
}
async function handleRoleChange(id: number, role: string) {
setError('');
const res = await fetch(`/api/admin-users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role }),
});
if (res.ok) {
const updated = await res.json();
setUsers((prev) => prev.map((u) => (u.id === id ? { ...u, role: updated.role } : u)));
} else {
const data = await res.json().catch(() => ({}));
setError(data.message ?? 'Kunde inte ändra roll');
}
}
async function handleResetPassword(id: number) {
setError('');
const res = await fetch(`/api/admin-users/${id}/reset-password`, { method: 'POST' });
const data = await res.json();
if (!res.ok) {
setError(data.message ?? 'Kunde inte återställa lösenord');
return;
}
setResetResult(data);
setCopiedBody(false);
setCopiedPw(false);
}
async function handleEmailSave(id: number) {
setError('');
const res = await fetch(`/api/admin-users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: editingEmail }),
});
if (res.ok) {
const updated = await res.json();
setUsers((prev) => prev.map((u) => (u.id === id ? { ...u, email: updated.email } : u)));
setEditingEmailId(null);
} else {
const data = await res.json().catch(() => ({}));
setError(data.message ?? 'Kunde inte uppdatera e-post');
}
}
return (
<div>
{/* Lägg till användare */}
<div style={{ marginBottom: '1.5rem' }}>
<button
onClick={() => setShowCreate((v) => !v)}
style={{
background: '#2563eb',
color: '#fff',
border: 'none',
borderRadius: 6,
padding: '0.5rem 1.2rem',
cursor: 'pointer',
fontWeight: 500,
}}
>
{showCreate ? ' Stäng' : '+ Lägg till användare'}
</button>
{showCreate && (
<form
onSubmit={handleCreate}
style={{
marginTop: '1rem',
background: '#f8fafc',
border: '1px solid #e2e8f0',
borderRadius: 8,
padding: '1rem 1.5rem',
maxWidth: 420,
}}
>
<h3 style={{ marginTop: 0, marginBottom: '1rem' }}>Ny användare</h3>
{createError && (
<div style={{ color: '#dc2626', marginBottom: '0.75rem', fontSize: 14 }}>
{createError}
</div>
)}
{[
{ key: 'username', label: 'Användarnamn', type: 'text' },
{ key: 'email', label: 'E-post', type: 'email' },
{ key: 'password', label: 'Lösenord', type: 'password' },
].map(({ key, label, type }) => (
<div key={key} style={{ marginBottom: '0.75rem' }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}>{label}</label>
<input
type={type}
value={(createForm as any)[key]}
onChange={(e) => setCreateForm((f) => ({ ...f, [key]: e.target.value }))}
required
style={{
width: '100%',
padding: '0.4rem 0.6rem',
border: '1px solid #cbd5e1',
borderRadius: 4,
fontSize: 14,
boxSizing: 'border-box',
}}
/>
</div>
))}
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}>Roll</label>
<select
value={createForm.role}
onChange={(e) => setCreateForm((f) => ({ ...f, role: e.target.value }))}
style={{
padding: '0.4rem 0.6rem',
border: '1px solid #cbd5e1',
borderRadius: 4,
fontSize: 14,
}}
>
<option value="user">Användare</option>
<option value="admin">Admin</option>
</select>
</div>
<button
type="submit"
disabled={creating}
style={{
background: '#16a34a',
color: '#fff',
border: 'none',
borderRadius: 6,
padding: '0.5rem 1.2rem',
cursor: 'pointer',
}}
>
{creating ? 'Skapar...' : 'Skapa'}
</button>
</form>
)}
</div>
{error && (
<div
style={{
background: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: 6,
padding: '0.6rem 1rem',
color: '#dc2626',
marginBottom: '1rem',
fontSize: 14,
}}
>
{error}
</div>
)}
{/* Användartabell */}
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
<thead>
<tr style={{ background: '#f1f5f9', textAlign: 'left' }}>
{['Användare', 'E-post', 'Roll', 'Åtgärder'].map((h) => (
<th
key={h}
style={{ padding: '0.6rem 0.8rem', borderBottom: '2px solid #e2e8f0' }}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{users.map((user) => {
const isSelf = user.id === currentUserId;
const isEditingEmail = editingEmailId === user.id;
return (
<tr
key={user.id}
style={{ borderBottom: '1px solid #e2e8f0', background: isSelf ? '#f0f9ff' : 'transparent' }}
>
{/* Namn */}
<td style={{ padding: '0.6rem 0.8rem' }}>
<div style={{ fontWeight: 500 }}>{user.username}</div>
{(user.firstName || user.lastName) && (
<div style={{ color: '#64748b', fontSize: 12 }}>
{[user.firstName, user.lastName].filter(Boolean).join(' ')}
</div>
)}
{isSelf && (
<span style={{ fontSize: 11, color: '#2563eb', fontStyle: 'italic' }}>
Du själv
</span>
)}
</td>
{/* E-post */}
<td style={{ padding: '0.6rem 0.8rem' }}>
{isEditingEmail ? (
<div style={{ display: 'flex', gap: 4 }}>
<input
type="email"
value={editingEmail}
onChange={(e) => setEditingEmail(e.target.value)}
style={{
padding: '0.3rem 0.5rem',
border: '1px solid #93c5fd',
borderRadius: 4,
fontSize: 13,
width: 180,
}}
/>
<button
onClick={() => handleEmailSave(user.id)}
style={{
background: '#16a34a',
color: '#fff',
border: 'none',
borderRadius: 4,
padding: '0.2rem 0.5rem',
cursor: 'pointer',
fontSize: 12,
}}
>
Spara
</button>
<button
onClick={() => setEditingEmailId(null)}
style={{
background: '#e2e8f0',
border: 'none',
borderRadius: 4,
padding: '0.2rem 0.5rem',
cursor: 'pointer',
fontSize: 12,
}}
>
Avbryt
</button>
</div>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ color: '#334155' }}>{user.email}</span>
{!isSelf && (
<button
onClick={() => {
setEditingEmailId(user.id);
setEditingEmail(user.email);
}}
title="Ändra e-post"
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
color: '#64748b',
fontSize: 13,
padding: 0,
}}
>
</button>
)}
</div>
)}
</td>
{/* Roll */}
<td style={{ padding: '0.6rem 0.8rem' }}>
{isSelf ? (
<RoleBadge role={user.role} />
) : (
<select
value={user.role}
onChange={(e) => handleRoleChange(user.id, e.target.value)}
style={{
padding: '0.25rem 0.4rem',
border: '1px solid #cbd5e1',
borderRadius: 4,
fontSize: 13,
background: user.role === 'admin' ? '#eff6ff' : '#f8fafc',
}}
>
<option value="user">Användare</option>
<option value="admin">Admin</option>
</select>
)}
</td>
{/* Åtgärder */}
<td style={{ padding: '0.6rem 0.8rem' }}>
{isSelf ? (
<span style={{ color: '#94a3b8', fontSize: 12 }}></span>
) : (
<div style={{ display: 'flex', gap: 6 }}>
<button
onClick={() => handleResetPassword(user.id)}
title="Återställ lösenord"
style={{
background: '#fef3c7',
border: '1px solid #fcd34d',
borderRadius: 4,
padding: '0.3rem 0.6rem',
cursor: 'pointer',
fontSize: 12,
color: '#92400e',
}}
>
Återställ lösenord
</button>
<button
onClick={() => handleDelete(user.id)}
title="Ta bort"
style={{
background: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: 4,
padding: '0.3rem 0.6rem',
cursor: 'pointer',
fontSize: 12,
color: '#dc2626',
}}
>
Ta bort
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Lösenordsåterställning modal */}
{resetResult && (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.45)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
>
<div
style={{
background: '#fff',
borderRadius: 10,
padding: '1.5rem 2rem',
maxWidth: 520,
width: '90%',
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
}}
>
<h3 style={{ marginTop: 0 }}>Lösenordet har återställts</h3>
<p style={{ fontSize: 13, color: '#475569' }}>
Skicka nedanstående meddelande till användaren och/eller ge dem det tillfälliga lösenordet.
</p>
<div style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<label style={{ fontSize: 13, fontWeight: 500 }}>Meddelande att skicka till användaren</label>
<button
onClick={() => {
navigator.clipboard.writeText(resetResult.body);
setCopiedBody(true);
}}
style={{
background: copiedBody ? '#dcfce7' : '#f1f5f9',
border: '1px solid #cbd5e1',
borderRadius: 4,
padding: '0.2rem 0.6rem',
cursor: 'pointer',
fontSize: 12,
color: copiedBody ? '#16a34a' : '#334155',
}}
>
{copiedBody ? '✓ Kopierat' : 'Kopiera meddelande'}
</button>
</div>
<textarea
readOnly
value={resetResult.body}
rows={6}
style={{
width: '100%',
padding: '0.5rem',
fontSize: 13,
fontFamily: 'monospace',
border: '1px solid #e2e8f0',
borderRadius: 6,
background: '#f8fafc',
resize: 'vertical',
boxSizing: 'border-box',
}}
/>
</div>
<div style={{ marginBottom: '1.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<label style={{ fontSize: 13, fontWeight: 500 }}>Tillfälligt lösenord</label>
<button
onClick={() => {
navigator.clipboard.writeText(resetResult.temporaryPassword);
setCopiedPw(true);
}}
style={{
background: copiedPw ? '#dcfce7' : '#f1f5f9',
border: '1px solid #cbd5e1',
borderRadius: 4,
padding: '0.2rem 0.6rem',
cursor: 'pointer',
fontSize: 12,
color: copiedPw ? '#16a34a' : '#334155',
}}
>
{copiedPw ? '✓ Kopierat' : 'Kopiera lösenord'}
</button>
</div>
<div
style={{
fontFamily: 'monospace',
fontSize: 18,
fontWeight: 700,
letterSpacing: 2,
padding: '0.5rem 1rem',
background: '#fff7ed',
border: '1px solid #fed7aa',
borderRadius: 6,
color: '#9a3412',
userSelect: 'all',
}}
>
{resetResult.temporaryPassword}
</div>
</div>
{!copiedBody && !copiedPw && (
<div
style={{
background: '#fefce8',
border: '1px solid #fde68a',
borderRadius: 6,
padding: '0.5rem 0.75rem',
fontSize: 12,
color: '#92400e',
marginBottom: '1rem',
}}
>
Kopiera lösenordet eller meddelandet innan du stänger det visas inte igen.
</div>
)}
<button
onClick={() => setResetResult(null)}
style={{
background: '#e2e8f0',
border: 'none',
borderRadius: 6,
padding: '0.5rem 1.2rem',
cursor: 'pointer',
fontWeight: 500,
}}
>
Stäng
</button>
</div>
</div>
)}
</div>
);
}
function RoleBadge({ role }: { role: string }) {
return (
<span
style={{
display: 'inline-block',
padding: '0.2rem 0.6rem',
borderRadius: 12,
fontSize: 12,
fontWeight: 600,
background: role === 'admin' ? '#eff6ff' : '#f0fdf4',
color: role === 'admin' ? '#1d4ed8' : '#15803d',
border: `1px solid ${role === 'admin' ? '#bfdbfe' : '#bbf7d0'}`,
}}
>
{role === 'admin' ? 'Admin' : 'Användare'}
</span>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { auth } from '../../../auth';
import AnvandareClient from './AnvandareClient';
export default async function AnvandareTab() {
const session = await auth();
const userId = (session?.user as any)?.userId as number | undefined;
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL_INTERNAL ?? 'http://recipe-api:8080'}/api/users`,
{
headers: { Authorization: `Bearer ${(session as any)?.accessToken}` },
cache: 'no-store',
},
);
const users = res.ok ? await res.json() : [];
return <AnvandareClient users={users} currentUserId={userId ?? 0} />;
}
+22
View File
@@ -0,0 +1,22 @@
import { fetchJson } from '../../../lib/api';
import type { Product } from '../../../features/inventory/types';
import MergePreviewForm from '../../admin/products/MergePreviewForm';
import AdminProductList from '../../admin/products/AdminProductList';
import ExpandableCreateProductSection from '../../admin/products/ExpandableCreateProductSection';
import ResetProductsButton from '../../admin/products/ResetProductsButton';
export default async function DatabsTab() {
const products = await fetchJson<Product[]>('/api/products');
return (
<div>
<p style={{ color: '#555', marginBottom: '1.5rem' }}>
Granska och standardisera produktnamn, slå ihop dubbletter och hantera kategorier.
</p>
<ExpandableCreateProductSection />
<ResetProductsButton />
<MergePreviewForm products={products} />
<AdminProductList products={products} />
</div>
);
}
+188
View File
@@ -0,0 +1,188 @@
'use client';
import { useState, useEffect, FormEvent } from 'react';
type Profile = {
id: number;
username: string;
email: string;
firstName: string | null;
lastName: string | null;
};
export default function ProfileClient() {
const [profile, setProfile] = useState<Profile | null>(null);
const [form, setForm] = useState({ firstName: '', lastName: '', email: '' });
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
fetch('/api/profile')
.then((r) => r.json())
.then((data: Profile) => {
setProfile(data);
setForm({
firstName: data.firstName ?? '',
lastName: data.lastName ?? '',
email: data.email,
});
})
.finally(() => setLoading(false));
}, []);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setError('');
setSuccess(false);
setSaving(true);
try {
const res = await fetch('/api/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
firstName: form.firstName || null,
lastName: form.lastName || null,
email: form.email,
}),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError(data.message ?? 'Kunde inte spara ändringar');
} else {
setSuccess(true);
}
} catch {
setError('Något gick fel');
} finally {
setSaving(false);
}
}
const inputStyle: React.CSSProperties = {
width: '100%',
padding: '10px 12px',
borderRadius: 6,
border: '1px solid #ddd',
fontSize: '1rem',
boxSizing: 'border-box',
};
const labelStyle: React.CSSProperties = {
display: 'block',
marginBottom: 6,
fontWeight: 500,
fontSize: '0.9rem',
color: '#444',
};
if (loading) {
return <p style={{ color: '#666' }}>Laddar profil...</p>;
}
return (
<div style={{ maxWidth: 480 }}>
{/* Initialer/avatar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}>
<div
style={{
width: 64,
height: 64,
borderRadius: '50%',
background: '#2563eb',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.5rem',
fontWeight: 700,
flexShrink: 0,
}}
>
{profile?.username?.charAt(0).toUpperCase()}
</div>
<div>
<div style={{ fontWeight: 600, fontSize: '1.1rem' }}>{profile?.username}</div>
<div style={{ color: '#666', fontSize: '0.9rem' }}>{profile?.email}</div>
</div>
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div>
<label htmlFor="firstName" style={labelStyle}>Förnamn</label>
<input
id="firstName"
type="text"
value={form.firstName}
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
style={inputStyle}
maxLength={100}
autoComplete="given-name"
/>
</div>
<div>
<label htmlFor="lastName" style={labelStyle}>Efternamn</label>
<input
id="lastName"
type="text"
value={form.lastName}
onChange={(e) => setForm((f) => ({ ...f, lastName: e.target.value }))}
style={inputStyle}
maxLength={100}
autoComplete="family-name"
/>
</div>
</div>
<div>
<label htmlFor="email" style={labelStyle}>E-post</label>
<input
id="email"
type="email"
value={form.email}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
required
style={inputStyle}
autoComplete="email"
/>
</div>
<div>
<label style={labelStyle}>Användarnamn</label>
<input
type="text"
value={profile?.username ?? ''}
disabled
style={{ ...inputStyle, background: '#f5f5f5', color: '#888', cursor: 'not-allowed' }}
/>
<p style={{ fontSize: '0.8rem', color: '#999', margin: '4px 0 0' }}>
Användarnamn kan inte ändras
</p>
</div>
{error && <p style={{ color: '#dc2626', margin: 0 }}>{error}</p>}
{success && <p style={{ color: '#16a34a', margin: 0 }}>Ändringarna sparades!</p>}
<button
type="submit"
disabled={saving}
style={{
padding: '10px',
background: '#2563eb',
color: 'white',
border: 'none',
borderRadius: 6,
fontSize: '1rem',
cursor: saving ? 'not-allowed' : 'pointer',
opacity: saving ? 0.7 : 1,
fontWeight: 500,
}}
>
{saving ? 'Sparar...' : 'Spara ändringar'}
</button>
</form>
</div>
);
}