Restyle users and profile workspace pages

This commit is contained in:
Flatlogic Bot 2026-06-09 13:21:14 +00:00
parent 82d5f49261
commit 5cc34803fe
2 changed files with 410 additions and 290 deletions

View File

@ -1,180 +1,265 @@
import {
mdiChartTimelineVariant,
mdiUpload,
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { mdiUpload } from '@mdi/js';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import BaseButton from '../components/BaseButton';
import FormCheckRadio from '../components/FormCheckRadio';
import FormCheckRadioGroup from '../components/FormCheckRadioGroup';
import FormImagePicker from '../components/FormImagePicker';
import { SwitchField } from '../components/SwitchField';
import { SelectField } from '../components/SelectField';
import { update, fetch } from '../stores/users/usersSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Head from 'next/head';
import Image from 'next/image';
import { useRouter } from 'next/router';
import {findMe} from "../stores/authSlice";
import React, { ReactElement, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import FormImagePicker from '../components/FormImagePicker';
import { SelectField } from '../components/SelectField';
import { SwitchField } from '../components/SwitchField';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
import { findMe } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { update } from '../stores/users/usersSlice';
type ProfileValues = {
firstName: string;
lastName: string;
phoneNumber: string;
email: string;
app_role: any;
disabled: boolean;
avatar: any[];
password: string;
};
const initVals: ProfileValues = {
firstName: '',
lastName: '',
phoneNumber: '',
email: '',
app_role: '',
disabled: false,
avatar: [],
password: '',
};
function Panel({
children,
className = '',
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<section
className={`rounded-lg border border-[#19192d]/10 bg-white ${className}`}
>
{children}
</section>
);
}
function FieldWrap({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<label className='block'>
<span className='mb-2 block text-sm font-semibold text-[#19192d]'>
{label}
</span>
{children}
</label>
);
}
const inputClassName =
'w-full rounded-lg border border-[#19192d]/10 bg-[#fffdf9] px-3 py-2 text-sm text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15 disabled:bg-[#fbf8f1] disabled:text-[#72798a]';
const EditUsers = () => {
const { currentUser, isFetching, token } = useAppSelector(
(state) => state.auth,
);
const router = useRouter();
const dispatch = useAppDispatch();
const notify = (type, msg) => toast(msg, { type });
const initVals = {
firstName: '',
lastName: '',
phoneNumber: '',
email: '',
app_role: '',
disabled: false,
avatar: [],
password: ''
};
const [initialValues, setInitialValues] = useState(initVals);
const { currentUser } = useAppSelector((state) => state.auth);
const router = useRouter();
const dispatch = useAppDispatch();
const [initialValues, setInitialValues] = useState(initVals);
useEffect(() => {
if (currentUser?.id && typeof currentUser === 'object') {
const newInitialVal = { ...initVals };
useEffect(() => {
if (currentUser?.id && typeof currentUser === 'object') {
const newInitialVal = { ...initVals } as Record<string, any>;
Object.keys(initVals).forEach(
(el) => (newInitialVal[el] = currentUser[el]),
);
Object.keys(initVals).forEach((key) => {
newInitialVal[key] = currentUser[key];
});
setInitialValues(newInitialVal);
}
}, [currentUser]);
setInitialValues(newInitialVal);
}
}, [currentUser]);
const handleSubmit = async (data) => {
await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe());
await router.push('/users/users-list');
notify('success', 'Profile was updated!');
};
const handleSubmit = async (data: ProfileValues) => {
await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe());
await router.push('/users/users-list');
toast('Profile was updated!', { type: 'success' });
};
return (
<>
<Head>
<title>{getPageTitle('Edit profile')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Edit profile'
main
const avatarUrl = currentUser?.avatar?.[0]?.publicUrl;
return (
<>
<Head>
<title>{getPageTitle('Edit profile')}</title>
</Head>
<main className='mx-auto max-w-5xl px-6 py-6'>
<div className='mb-4 rounded-lg bg-[#19192d] p-5 text-white'>
<div className='flex items-center gap-3 text-[#b17a1e]'>
<span className='text-lg'></span>
<span className='text-xs font-semibold uppercase tracking-[0.22em]'>
Account
</span>
</div>
<h1 className='mt-3 text-xl font-semibold'>Profile</h1>
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
Update your workspace profile, avatar, role, and password.
</p>
</div>
<Panel className='p-5'>
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form className='grid gap-5'>
<div className='grid gap-5 md:grid-cols-[220px_1fr]'>
<div>
<p className='text-sm font-semibold text-[#19192d]'>Avatar</p>
<div className='mt-3 flex h-32 w-32 items-center justify-center overflow-hidden rounded-full border border-[#19192d]/10 bg-[#f3fbf8]'>
{avatarUrl ? (
<Image
className='h-full w-full object-cover object-center'
src={avatarUrl}
alt='Avatar'
width={128}
height={128}
unoptimized
/>
) : (
<span className='text-3xl font-semibold text-[#35b7a5]'>
{currentUser?.firstName?.[0] ||
currentUser?.email?.[0] ||
'U'}
</span>
)}
</div>
<div className='mt-4'>
<Field
label='Avatar'
color='info'
icon={mdiUpload}
path='users/avatar'
name='avatar'
id='avatar'
schema={{
size: undefined,
formats: undefined,
}}
component={FormImagePicker}
/>
</div>
</div>
<div className='grid gap-4 md:grid-cols-2'>
<FieldWrap label='First name'>
<Field
className={inputClassName}
name='firstName'
placeholder='First name'
/>
</FieldWrap>
<FieldWrap label='Last name'>
<Field
className={inputClassName}
name='lastName'
placeholder='Last name'
/>
</FieldWrap>
<FieldWrap label='Phone number'>
<Field
className={inputClassName}
name='phoneNumber'
placeholder='Phone number'
/>
</FieldWrap>
<FieldWrap label='Email'>
<Field
className={inputClassName}
name='email'
placeholder='Email'
disabled
/>
</FieldWrap>
<FieldWrap label='App role'>
<Field
name='app_role'
id='app_role'
component={SelectField}
options={initialValues.app_role}
itemRef='roles'
showField='name'
/>
</FieldWrap>
<FieldWrap label='Password'>
<Field
className={inputClassName}
name='password'
placeholder='New password'
/>
</FieldWrap>
<div className='md:col-span-2'>
<FieldWrap label='Disabled'>
<Field
name='disabled'
id='disabled'
component={SwitchField}
/>
</FieldWrap>
</div>
</div>
</div>
<div className='flex flex-wrap justify-end gap-3 border-t border-[#19192d]/10 pt-5'>
<button
type='button'
className='rounded-full border border-[#19192d]/10 bg-white px-4 py-2 text-sm font-semibold text-[#19192d]'
onClick={() => router.push('/users/users-list')}
>
{''}
</SectionTitleLineWithButton>
<CardBox>
{currentUser?.avatar[0]?.publicUrl && <div className={'grid grid-cols-6 gap-4 mb-4'}>
<div className="col-span-1 w-80 h-80 overflow-hidden border-2 rounded-full inline-flex items-center justify-center mb-8">
<img className="w-80 h-80 max-w-full max-h-full object-cover object-center" src={`${currentUser?.avatar[0]?.publicUrl}`} alt="Avatar" />
</div>
</div>}
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField>
<Field
label='Avatar'
color='info'
icon={mdiUpload}
path={'users/avatar'}
name='avatar'
id='avatar'
schema={{
size: undefined,
formats: undefined,
}}
component={FormImagePicker}
></Field>
</FormField>
<FormField label='First Name'>
<Field name='firstName' placeholder='First Name' />
</FormField>
<FormField label='Last Name'>
<Field name='lastName' placeholder='Last Name' />
</FormField>
<FormField label='Phone Number'>
<Field name='phoneNumber' placeholder='Phone Number' />
</FormField>
<FormField label='E-Mail'>
<Field name='email' placeholder='E-Mail' disabled />
</FormField>
<FormField label='App Role' labelFor='app_role'>
<Field
name='app_role'
id='app_role'
component={SelectField}
options={initialValues.app_role}
itemRef={'roles'}
showField={'name'}
></Field>
</FormField>
<FormField label='Disabled' labelFor='disabled'>
<Field
name='disabled'
id='disabled'
component={SwitchField}
></Field>
</FormField>
<FormField
label="Password"
>
<Field
name="password"
placeholder="password"
/>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/users/users-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
Cancel
</button>
<button
type='reset'
className='rounded-full border border-[#19192d]/10 bg-white px-4 py-2 text-sm font-semibold text-[#19192d]'
>
Reset
</button>
<button
type='submit'
className='rounded-full bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white'
>
Save profile
</button>
</div>
</Form>
</Formik>
</Panel>
</main>
</>
);
};
EditUsers.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default EditUsers;

View File

@ -1,166 +1,201 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import axios from 'axios';
import Head from 'next/head';
import React, { ReactElement, useState } from 'react';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableUsers from '../../components/Users/TableUsers'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/users/usersSlice';
import CardBoxModal from '../../components/CardBoxModal';
import DragDropFilePicker from '../../components/DragDropFilePicker';
import TableUsers from '../../components/Users/TableUsers';
import { getPageTitle } from '../../config';
import { hasPermission } from '../../helpers/userPermissions';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { setRefetch, uploadCsv } from '../../stores/users/usersSlice';
function Panel({
children,
className = '',
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<section
className={`rounded-lg border border-[#19192d]/10 bg-white ${className}`}
>
{children}
</section>
);
}
import {hasPermission} from "../../helpers/userPermissions";
function ActionButton({
children,
href,
onClick,
variant = 'primary',
}: {
children: React.ReactNode;
href?: string;
onClick?: () => void;
variant?: 'primary' | 'secondary';
}) {
const className =
variant === 'primary'
? 'rounded-full bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white'
: 'rounded-full border border-[#19192d]/10 bg-white px-4 py-2 text-sm font-semibold text-[#19192d]';
if (href) {
return (
<a href={href} className={className}>
{children}
</a>
);
}
return (
<button type='button' className={className} onClick={onClick}>
{children}
</button>
);
}
const UsersTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'First Name', title: 'firstName'},{label: 'Last Name', title: 'lastName'},{label: 'Phone Number', title: 'phoneNumber'},{label: 'E-Mail', title: 'email'},
{label: 'App Role', title: 'app_role'},
{label: 'Custom Permissions', title: 'custom_permissions'},
const [filters] = useState([
{ label: 'First Name', title: 'firstName' },
{ label: 'Last Name', title: 'lastName' },
{ label: 'Phone Number', title: 'phoneNumber' },
{ label: 'E-Mail', title: 'email' },
{ label: 'App Role', title: 'app_role' },
{ label: 'Custom Permissions', title: 'custom_permissions' },
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const hasCreatePermission =
currentUser && hasPermission(currentUser, 'CREATE_USERS');
const getUsersCSV = async () => {
const response = await axios({url: '/users?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'usersCSV.csv'
link.click()
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const getUsersCSV = async () => {
const response = await axios({
url: '/users?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'usersCSV.csv';
link.click();
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const onModalConfirm = async () => {
if (!csvFile) {
return;
}
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
return (
<>
<Head>
<title>{getPageTitle('Users')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main>
{''}
</SectionTitleLineWithButton>
<CardBox id="usersList" className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label='Add/Invite User'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getUsersCSV} />
<main className='mx-auto max-w-7xl px-6 py-6'>
<div className='mb-4 rounded-lg bg-[#19192d] p-5 text-white'>
<div className='flex items-center gap-3 text-[#b17a1e]'>
<span className='text-lg'></span>
<span className='text-xs font-semibold uppercase tracking-[0.22em]'>
Team access
</span>
</div>
<h1 className='mt-3 text-xl font-semibold'>Users</h1>
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
Manage workspace members, roles, and export or import user records.
</p>
</div>
<Panel className='mb-4 p-4'>
<div className='flex flex-wrap items-center gap-3'>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
<ActionButton href='/users/users-new'>Add user</ActionButton>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
<ActionButton variant='secondary' onClick={addFilter}>
Filter
</ActionButton>
<ActionButton variant='secondary' onClick={getUsersCSV}>
Download CSV
</ActionButton>
{hasCreatePermission && (
<ActionButton
variant='secondary'
onClick={() => setIsModalActive(true)}
>
Upload CSV
</ActionButton>
)}
<div className='ms-auto' id='delete-rows-button'></div>
</div>
</Panel>
<Panel className='overflow-hidden'>
<TableUsers
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</Panel>
</main>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel='Confirm'
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats='.csv'
/>
</CardBoxModal>
</>
)
}
);
};
UsersTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_USERS'}
>
{page}
</LayoutAuthenticated>
)
}
<LayoutAuthenticated permission='READ_USERS'>{page}</LayoutAuthenticated>
);
};
export default UsersTablesPage
export default UsersTablesPage;