feat: add member since date handling across admin and member views
This commit is contained in:
@@ -385,13 +385,15 @@ const AdminMembers = () => {
|
||||
</div>
|
||||
) : filteredUsers.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredUsers.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`member-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
{filteredUsers.map((user) => {
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
return (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`member-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
@@ -409,7 +411,7 @@ const AdminMembers = () => {
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple dark:text-brand-lavender " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
{user.referred_by_member_name && (
|
||||
<p>Referred by: {user.referred_by_member_name}</p>
|
||||
)}
|
||||
@@ -523,7 +525,8 @@ const AdminMembers = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
|
||||
@@ -242,12 +242,14 @@ const AdminStaff = () => {
|
||||
</div>
|
||||
) : filteredUsers.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredUsers.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`staff-card-${user.id}`}
|
||||
>
|
||||
{filteredUsers.map((user) => {
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
return (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`staff-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
@@ -267,7 +269,7 @@ const AdminStaff = () => {
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
{user.last_login && (
|
||||
<p>Last Login: {new Date(user.last_login).toLocaleDateString()}</p>
|
||||
)}
|
||||
@@ -321,8 +323,9 @@ const AdminStaff = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '../../components/ui/avatar';
|
||||
import { Input } from '../../components/ui/input';
|
||||
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2, Shield } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
@@ -24,15 +25,31 @@ const AdminUserView = () => {
|
||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
|
||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
|
||||
const [memberSince, setMemberSince] = useState('');
|
||||
const [memberSinceSaving, setMemberSinceSaving] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
const [changeRoleDialogOpen, setChangeRoleDialogOpen] = useState(false);
|
||||
|
||||
const formatDateInputValue = (value) => {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') {
|
||||
return value.slice(0, 10);
|
||||
}
|
||||
return new Date(value).toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchUserProfile();
|
||||
fetchSubscriptions();
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setMemberSince(formatDateInputValue(user.member_since));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const response = await api.get(`/admin/users/${userId}`);
|
||||
@@ -177,6 +194,27 @@ const AdminUserView = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMemberSinceSave = async () => {
|
||||
if (!user) return;
|
||||
setMemberSinceSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
member_since: memberSince ? memberSince : null
|
||||
};
|
||||
const response = await api.put(`/admin/users/${userId}`, payload);
|
||||
setUser(prev => ({
|
||||
...prev,
|
||||
...(response?.data || {}),
|
||||
member_since: payload.member_since
|
||||
}));
|
||||
toast.success('Member since updated successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to update member since');
|
||||
} finally {
|
||||
setMemberSinceSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getActionMessage = () => {
|
||||
if (!pendingAction || !user) return {};
|
||||
|
||||
@@ -212,6 +250,10 @@ const AdminUserView = () => {
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (!user) return null;
|
||||
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
const memberSinceBaseline = formatDateInputValue(user.member_since);
|
||||
const memberSinceHasChanges = memberSince !== memberSinceBaseline;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Back Button */}
|
||||
@@ -262,7 +304,7 @@ const AdminUserView = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Joined {new Date(user.created_at).toLocaleDateString()}</span>
|
||||
<span>Joined {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,6 +405,27 @@ const AdminUserView = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</label>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
type="date"
|
||||
value={memberSince}
|
||||
onChange={(e) => setMemberSince(e.target.value)}
|
||||
className="max-w-[200px] border-[var(--neutral-800)]"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleMemberSinceSave}
|
||||
disabled={memberSinceSaving || !memberSinceHasChanges}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
>
|
||||
{memberSinceSaving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user.partner_first_name && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Partner</label>
|
||||
|
||||
Reference in New Issue
Block a user