whisper-money/resources/js/pages/settings/spaces.tsx

316 lines
13 KiB
TypeScript

import {
destroy as destroySpace,
select as selectSpace,
index as spacesIndex,
store as storeSpace,
update as updateSpace,
} from '@/actions/App/Http/Controllers/SpaceController';
import HeadingSmall from '@/components/heading-small';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import { type BreadcrumbItem, type SharedData, type Space } from '@/types';
import { __ } from '@/utils/i18n';
import { Head, router, usePage } from '@inertiajs/react';
import { Building2, Check, User } from 'lucide-react';
import { FormEvent, useState } from 'react';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Spaces settings',
href: spacesIndex().url,
},
];
export default function Spaces() {
const { spaces, currentSpace } = usePage<SharedData>().props;
const [createOpen, setCreateOpen] = useState(false);
const [renaming, setRenaming] = useState<Space | null>(null);
const [deleting, setDeleting] = useState<Space | null>(null);
const [name, setName] = useState('');
const [processing, setProcessing] = useState(false);
const submitCreate = (event: FormEvent) => {
event.preventDefault();
setProcessing(true);
router.post(
storeSpace().url,
{ name },
{
preserveScroll: true,
onFinish: () => setProcessing(false),
onSuccess: () => {
setCreateOpen(false);
setName('');
},
},
);
};
const submitRename = (event: FormEvent) => {
event.preventDefault();
if (!renaming) {
return;
}
setProcessing(true);
router.patch(
updateSpace(renaming.id).url,
{ name },
{
preserveScroll: true,
onFinish: () => setProcessing(false),
onSuccess: () => setRenaming(null),
},
);
};
const confirmDelete = () => {
if (!deleting) {
return;
}
setProcessing(true);
router.delete(destroySpace(deleting.id).url, {
preserveScroll: true,
onFinish: () => setProcessing(false),
onSuccess: () => setDeleting(null),
});
};
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title={__('Spaces')} />
<SettingsLayout>
<div className="space-y-6">
<div className="flex items-start justify-between gap-4">
<HeadingSmall
title={__('Spaces')}
description={__(
'Each space keeps its own accounts, transactions, categories and budgets separate.',
)}
/>
<Button onClick={() => setCreateOpen(true)}>
{__('Create space')}
</Button>
</div>
<ul className="divide-y divide-border rounded-lg border border-border">
{spaces.map((space) => {
const isCurrent = space.id === currentSpace?.id;
return (
<li
key={space.id}
className="flex items-center gap-3 px-4 py-3"
data-test="space-row"
>
<div className="flex size-9 items-center justify-center rounded-lg bg-muted text-muted-foreground">
{space.personal ? (
<User className="size-4" />
) : (
<Building2 className="size-4" />
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 font-medium">
{space.name}
{isCurrent && (
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
<Check className="size-3" />
{__('Active')}
</span>
)}
</div>
<div className="text-xs text-muted-foreground">
{space.personal
? __('Personal space')
: __('Space')}
</div>
</div>
<div className="flex items-center gap-2">
{!isCurrent && (
<Button
variant="outline"
size="sm"
onClick={() =>
router.post(
selectSpace(space.id)
.url,
)
}
>
{__('Switch')}
</Button>
)}
{!space.personal && space.is_owner && (
<>
<Button
variant="outline"
size="sm"
onClick={() => {
setName(space.name);
setRenaming(space);
}}
>
{__('Rename')}
</Button>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
onClick={() =>
setDeleting(space)
}
>
{__('Delete')}
</Button>
</>
)}
</div>
</li>
);
})}
</ul>
</div>
</SettingsLayout>
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent>
<form onSubmit={submitCreate}>
<DialogHeader>
<DialogTitle>{__('Create space')}</DialogTitle>
<DialogDescription>
{__(
'A space groups its own accounts, transactions, categories and budgets, kept separate from your other spaces.',
)}
</DialogDescription>
</DialogHeader>
<div className="grid gap-2 py-4">
<Label htmlFor="create-space-name">
{__('Name')}
</Label>
<Input
id="create-space-name"
value={name}
onChange={(event) =>
setName(event.target.value)
}
placeholder={__('e.g. My Company')}
autoFocus
required
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setCreateOpen(false)}
>
{__('Cancel')}
</Button>
<Button
type="submit"
disabled={processing || name.trim() === ''}
>
{__('Create space')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog
open={renaming !== null}
onOpenChange={(open) => !open && setRenaming(null)}
>
<DialogContent>
<form onSubmit={submitRename}>
<DialogHeader>
<DialogTitle>{__('Rename space')}</DialogTitle>
</DialogHeader>
<div className="grid gap-2 py-4">
<Label htmlFor="rename-space-name">
{__('Name')}
</Label>
<Input
id="rename-space-name"
value={name}
onChange={(event) =>
setName(event.target.value)
}
autoFocus
required
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setRenaming(null)}
>
{__('Cancel')}
</Button>
<Button
type="submit"
disabled={processing || name.trim() === ''}
>
{__('Save')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<AlertDialog
open={deleting !== null}
onOpenChange={(open) => !open && setDeleting(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{__('Delete space')}
</AlertDialogTitle>
<AlertDialogDescription>
{__('Delete this space? This cannot be undone.')}
{deleting ? ` (${deleting.name})` : ''}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={processing}>
{__('Cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={processing}
variant="destructive"
>
{__('Delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</AppLayout>
);
}