infoalloggi-monorepo/apps/infoalloggi/src/pages/area-riservata/admin/storage.tsx

404 lines
10 KiB
TypeScript
Raw Normal View History

2025-08-28 18:27:07 +02:00
import { ChevronsUpDown, RefreshCcw } from "lucide-react";
import type { GetServerSideProps } from "next";
2025-08-04 17:45:44 +02:00
import { useState } from "react";
2025-08-28 18:27:07 +02:00
import toast from "react-hot-toast";
2025-08-04 17:45:44 +02:00
import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { StorageTable } from "~/components/tables/storage-table";
import { Button } from "~/components/ui/button";
import {
2025-08-28 18:27:07 +02:00
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/command";
import {
2025-08-28 18:27:07 +02:00
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { Separator } from "~/components/ui/separator";
2025-08-04 17:45:44 +02:00
import { UploadModal } from "~/components/upload_modal";
import { UserAvatar } from "~/components/user_avatar";
2025-08-28 18:27:07 +02:00
import { cn } from "~/lib/utils";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import {
StorageTableProvider,
type StorageTable as StorageTableType,
useStorageTable,
} from "~/providers/StorageTableProvider";
2025-10-24 16:10:59 +02:00
2025-08-28 18:27:07 +02:00
import type { Users } from "~/schemas/public/Users";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
2025-08-28 18:27:07 +02:00
import { api } from "~/utils/api";
2025-08-04 17:45:44 +02:00
const AdminStorage: NextPageWithLayout = () => {
2025-08-28 18:27:07 +02:00
const { data, isLoading, refetch } = api.storage.getAll.useQuery();
const { t } = useTranslation();
const utils = api.useUtils();
2025-10-24 16:10:59 +02:00
const { mutate: deleteFiles } = api.storage.deleteFile.useMutation({
2025-08-28 18:27:07 +02:00
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getAll.invalidate();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
id: context?.toastId,
});
},
});
2025-08-04 17:45:44 +02:00
2025-10-24 16:10:59 +02:00
const handleDelete = (id: string) => {
deleteFiles({ storageId: id });
2025-08-28 18:27:07 +02:00
};
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const handleRefetch = async () => {
await refetch();
};
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if (isLoading) return <LoadingPage />;
if (!data) return <Status500 />;
//magari query amche file ownership per poter filtrare in tab se da admin o da utente o una lista di utenti a cui è visibile il file
return (
<StorageTableProvider>
<div className="mx-1 flex-1 overflow-auto">
<div className="mx-auto py-8">
<StorageTableHeader
handleDelete={handleDelete}
2025-08-29 16:18:32 +02:00
handleRefetch={handleRefetch}
2025-08-28 18:27:07 +02:00
/>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<StorageTable data={data} handleDelete={handleDelete} />
</div>
</div>
</StorageTableProvider>
);
2025-08-04 17:45:44 +02:00
};
export const getServerSideProps = (async (context) => {
const access_token = context.req.cookies.access_token;
const helpers = await TrpcAuthedFetchingIstance({ access_token });
if (helpers) {
await helpers.trpc.storage.getAll.prefetch();
return {
props: {
trpcState: helpers.trpc.dehydrate(),
},
};
}
return {
props: {},
};
}) satisfies GetServerSideProps;
2025-08-04 17:45:44 +02:00
export default AdminStorage;
const StorageTableHeader = ({
2025-08-28 18:27:07 +02:00
handleRefetch,
handleDelete,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
handleRefetch: () => Promise<void>;
2025-10-24 16:10:59 +02:00
handleDelete: (id: string) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { table } = useStorageTable();
if (!table) return null;
return (
<div className="mx-3 flex flex-wrap items-center justify-between gap-3">
<div className="flex max-w-lg items-center gap-3">
2025-10-10 16:18:43 +02:00
<h3 className="font-bold text-accent-foreground text-xl sm:text-2xl">
2025-08-28 18:27:07 +02:00
Impostazioni
</h3>
<button
2025-08-29 16:18:32 +02:00
className="cursor-pointer hover:scale-110"
2025-08-28 18:27:07 +02:00
onClick={async () => {
await handleRefetch();
table.resetRowSelection();
}}
2025-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
>
<RefreshCcw className="size-6" />
</button>
</div>
<div className="flex max-w-lg flex-wrap items-center gap-3">
<UploadModal
cb_onUpload={() => {
table.setRowSelection({});
}}
/>
<Actions handleDelete={handleDelete} table={table} />
</div>
</div>
);
2025-08-04 17:45:44 +02:00
};
AdminStorage.getLayout = function getLayout(page) {
2025-08-28 18:27:07 +02:00
return <AreaRiservataLayout>{page}</AreaRiservataLayout>;
2025-08-04 17:45:44 +02:00
};
const Actions = ({
2025-08-28 18:27:07 +02:00
handleDelete,
table,
2025-08-04 17:45:44 +02:00
}: {
2025-10-24 16:10:59 +02:00
handleDelete: (id: string) => void;
2025-08-28 18:27:07 +02:00
table: StorageTableType;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { selectedRows } = useStorageTable();
const [visbOpen, setVisbOpen] = useState(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const deleteSelected = () => {
selectedRows.forEach((row) => handleDelete(row.id));
table.resetRowSelection();
};
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<div className="flex items-center gap-4 md:mt-0">
<Button
className="transition-all duration-100"
2025-08-29 16:18:32 +02:00
disabled={selectedRows.length === 0}
2025-08-28 18:27:07 +02:00
onClick={() => setVisbOpen(true)}
2025-08-29 16:18:32 +02:00
variant="info"
2025-08-28 18:27:07 +02:00
>
Modifica Visibilità
</Button>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<Button
2025-08-29 16:18:32 +02:00
className="transition-all duration-100"
2025-08-28 18:27:07 +02:00
disabled={selectedRows.length === 0}
onClick={() => deleteSelected()}
2025-08-29 16:18:32 +02:00
variant="destructive"
2025-08-28 18:27:07 +02:00
>
Elimina
</Button>
{visbOpen && (
<VisibComponent
open={visbOpen}
setOpen={setVisbOpen}
2025-10-24 16:10:59 +02:00
storageIds={selectedRows.map((r) => r.id)}
2025-08-28 18:27:07 +02:00
/>
)}
</div>
);
2025-08-04 17:45:44 +02:00
};
const VisibComponent = ({
2025-10-24 16:10:59 +02:00
storageIds,
2025-08-28 18:27:07 +02:00
open,
setOpen,
2025-08-04 17:45:44 +02:00
}: {
2025-10-24 16:10:59 +02:00
storageIds: string[];
2025-08-28 18:27:07 +02:00
open: boolean;
setOpen: (v: boolean) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
const { data, isLoading } = api.storage.getStorageVisibility.useQuery({
2025-10-24 16:10:59 +02:00
storageIds,
2025-08-28 18:27:07 +02:00
});
const { data: users } = api.users.getUsers.useQuery();
2025-08-04 17:45:44 +02:00
2025-10-24 16:10:59 +02:00
const { mutate: remove } = api.storage.deleteUserStorageEntry.useMutation({
2025-08-28 18:27:07 +02:00
onMutate: () => {
const toastId = toast.loading("Rimozione utente in corso", {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageVisibility.invalidate();
toast.success("Utente rimosso", {
icon: "🗑️",
id: context?.toastId,
});
},
});
2025-10-24 16:10:59 +02:00
const { mutate: add } = api.storage.addUserStorage.useMutation({
2025-08-28 18:27:07 +02:00
onMutate: () => {
const toastId = toast.loading("Aggiunta utente in corso", {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageVisibility.invalidate();
toast.success("Utente aggiunto", {
icon: "👍",
id: context?.toastId,
});
},
});
2025-08-04 17:45:44 +02:00
2025-10-24 16:10:59 +02:00
const { mutate: reset } = api.storage.deleteFilesFromUserStorage.useMutation({
2025-08-28 18:27:07 +02:00
onMutate: () => {
const toastId = toast.loading("Reset visibilità in corso", {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageVisibility.invalidate();
toast.success("Reset effettuato", {
icon: "👍",
id: context?.toastId,
});
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<>
2025-08-29 16:18:32 +02:00
<Dialog onOpenChange={setOpen} open={open}>
2025-08-28 18:27:07 +02:00
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Modifica Visibilità</DialogTitle>
<DialogDescription className="sr-only">
Modifica la visibilità degli allegati selezionati
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
{isLoading ? (
<LoadingPage />
) : (
<>
<div className="flex items-center gap-3">
<p>Seleziona utente:</p>
<UtentiCombo
onSelect={(user) => {
2025-10-24 16:10:59 +02:00
for (const allegato of storageIds) {
add({
storageId: allegato,
userId: user.id,
from_admin: true,
});
2025-08-28 18:27:07 +02:00
}
}}
2025-08-29 16:18:32 +02:00
users={users || []}
2025-08-28 18:27:07 +02:00
/>
<Button
className="ml-auto"
2025-10-24 16:10:59 +02:00
onClick={() => reset({ storageIds })}
2025-08-29 16:18:32 +02:00
variant="destructive"
2025-08-28 18:27:07 +02:00
>
Reset
</Button>
</div>
{data?.map((allegato, i) => (
<div
className={cn(
"flex flex-col gap-3 rounded-md p-2",
i % 2 === 0 && "bg-gray-200",
)}
2025-08-29 16:18:32 +02:00
key={allegato.id}
2025-08-28 18:27:07 +02:00
>
2025-10-10 16:18:43 +02:00
<p className="font-semibold text-lg">
2025-10-24 16:10:59 +02:00
File: {allegato.originalName}
2025-08-28 18:27:07 +02:00
</p>
<div className="flex items-center gap-3">
<p>Visibile a:</p>
2025-10-24 16:10:59 +02:00
{allegato.users.length === 0 ? (
2025-08-28 18:27:07 +02:00
<p>Nessuno</p>
) : (
<ul className="flex gap-2">
2025-10-24 16:10:59 +02:00
{allegato.users.map((user) => (
<li key={user.id}>
2025-08-28 18:27:07 +02:00
<Button
className="after:content-['X']"
onClick={() =>
remove({
storageId: allegato.id,
2025-10-24 16:10:59 +02:00
userId: user.id,
2025-08-28 18:27:07 +02:00
})
}
2025-08-29 16:18:32 +02:00
variant="outline"
2025-08-28 18:27:07 +02:00
>
{user.username}
</Button>
</li>
))}
</ul>
)}
</div>
</div>
))}
</>
)}
<Separator />
</div>
</DialogContent>
</Dialog>
</>
);
2025-08-04 17:45:44 +02:00
};
const UtentiCombo = ({
2025-08-28 18:27:07 +02:00
users,
onSelect,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
users: Users[];
onSelect: (user: Users) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const [open, setOpen] = useState(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
2025-08-29 16:18:32 +02:00
<Popover onOpenChange={setOpen} open={open}>
2025-08-28 18:27:07 +02:00
<PopoverTrigger asChild>
<Button
aria-expanded={open}
className="w-auto justify-between"
2025-08-29 16:18:32 +02:00
role="combobox"
variant="outline"
2025-08-28 18:27:07 +02:00
>
Seleziona utente...
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Command>
<CommandInput placeholder="Cerca Utente..." />
<CommandList>
<CommandEmpty>Nessun utente.</CommandEmpty>
<CommandGroup>
{users.map((user) => (
<CommandItem
key={user.id}
onSelect={(currentValue) => {
// biome-ignore lint/style/noNonNullAssertion: <known lenght>
onSelect(users.find((u) => u.id === currentValue)!);
setOpen(false);
}}
2025-08-29 16:18:32 +02:00
value={user.id}
2025-08-28 18:27:07 +02:00
>
<span className="flex items-center gap-2">
<UserAvatar
className="size-8"
userId={user.id}
username={user.username}
/>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<span>
<span className="block font-medium">{user.username}</span>
</span>
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
2025-08-04 17:45:44 +02:00
};