2025-08-28 18:27:07 +02:00
|
|
|
import { format } from "date-fns";
|
|
|
|
|
import {
|
|
|
|
|
Download,
|
|
|
|
|
File,
|
|
|
|
|
FileArchive,
|
|
|
|
|
FileAudio,
|
|
|
|
|
FileCode,
|
|
|
|
|
FileSpreadsheet,
|
|
|
|
|
FileText,
|
|
|
|
|
FileVideo,
|
|
|
|
|
ImageIcon,
|
|
|
|
|
MoreVertical,
|
|
|
|
|
Pencil,
|
|
|
|
|
Trash2,
|
|
|
|
|
TriangleAlert,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { type FormEvent, useState } from "react";
|
2025-08-04 17:45:44 +02:00
|
|
|
import toast from "react-hot-toast";
|
2025-08-28 18:27:07 +02:00
|
|
|
import Input from "~/components/custom_ui/input";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { LoadingPage } from "~/components/loading";
|
|
|
|
|
import { Status500 } from "~/components/status-page";
|
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/components/ui/dialog";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/components/ui/dropdown-menu";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { Separator } from "~/components/ui/separator";
|
|
|
|
|
import { UploadModal } from "~/components/upload_modal";
|
|
|
|
|
import { handleDownload } from "~/hooks/filesHooks";
|
|
|
|
|
import { cn } from "~/lib/utils";
|
|
|
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
|
|
|
|
import type {
|
|
|
|
|
Storageindex,
|
|
|
|
|
StorageindexId,
|
|
|
|
|
} from "~/schemas/public/Storageindex";
|
|
|
|
|
import type { UsersId } from "~/schemas/public/Users";
|
|
|
|
|
import { api } from "~/utils/api";
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
export const AllegatiComp = ({
|
2025-08-28 18:27:07 +02:00
|
|
|
userId,
|
|
|
|
|
isAdmin,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
userId: UsersId;
|
|
|
|
|
isAdmin: boolean;
|
2025-08-04 17:45:44 +02:00
|
|
|
}) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const { data: allegati, isLoading: isLoadingAllegati } =
|
|
|
|
|
api.storage.getUserStorage.useQuery({ userId: userId });
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const { mutate: deleteFile } = api.storage.deleteUserStorage.useMutation({
|
|
|
|
|
onMutate: () => {
|
|
|
|
|
const toastId = toast.loading(t.caricamento, {
|
|
|
|
|
icon: "🪛",
|
|
|
|
|
});
|
|
|
|
|
return { toastId };
|
|
|
|
|
},
|
|
|
|
|
onSuccess: async (_error, _variables, context) => {
|
|
|
|
|
await utils.storage.getUserStorage.invalidate({
|
|
|
|
|
userId: userId,
|
|
|
|
|
});
|
|
|
|
|
toast.success(t.allegati.allegato_rimosso, {
|
|
|
|
|
icon: "🗑️",
|
|
|
|
|
id: context?.toastId,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const { mutate: renameFile } = api.storage.renameFile.useMutation({
|
|
|
|
|
onMutate: () => {
|
|
|
|
|
setEditOpen(false);
|
|
|
|
|
const toastId = toast.loading(t.caricamento, {
|
|
|
|
|
icon: "🪛",
|
|
|
|
|
});
|
|
|
|
|
return { toastId };
|
|
|
|
|
},
|
|
|
|
|
onSuccess: async (_error, _variables, context) => {
|
|
|
|
|
await utils.storage.getUserStorage.invalidate({
|
|
|
|
|
userId: userId,
|
|
|
|
|
});
|
|
|
|
|
toast.success(t.successo, {
|
|
|
|
|
icon: "👍",
|
|
|
|
|
id: context?.toastId,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const [editData, setEditData] = useState<{
|
|
|
|
|
id: StorageindexId;
|
|
|
|
|
filename: string;
|
|
|
|
|
} | null>(null);
|
|
|
|
|
const [editOpen, setEditOpen] = useState(false);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const handleEditSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const form = e.currentTarget;
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
if (!formData.get("id") || !formData.get("filename")) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
renameFile({
|
|
|
|
|
fileId: formData.get("id") as StorageindexId,
|
|
|
|
|
name: formData.get("filename") as string,
|
|
|
|
|
});
|
|
|
|
|
};
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const utils = api.useUtils();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
if (isLoadingAllegati) return <LoadingPage />;
|
|
|
|
|
if (!allegati) return <Status500 />;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const from_admin = allegati.filter((a) => a.from_admin);
|
|
|
|
|
const not_from_admin = allegati.filter((a) => !a.from_admin);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col space-y-5">
|
2025-10-10 16:18:43 +02:00
|
|
|
<h1 className="font-bold text-2xl text-primary">{t.allegati.title}</h1>
|
|
|
|
|
<h1 className="font-semibold text-xl">{t.allegati.da_info}</h1>
|
2025-08-28 18:27:07 +02:00
|
|
|
{from_admin && (
|
|
|
|
|
<FileList
|
2025-08-29 16:18:32 +02:00
|
|
|
delete_onClick={(id) => deleteFile({ fileId: id, userId })}
|
2025-08-28 18:27:07 +02:00
|
|
|
download_onClick={(file) =>
|
|
|
|
|
handleDownload({
|
|
|
|
|
file,
|
|
|
|
|
getToken: async () => await getToken(),
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-08-29 16:18:32 +02:00
|
|
|
files={from_admin}
|
|
|
|
|
isAdmin={isAdmin}
|
|
|
|
|
selectHandler={(id, filename) => {
|
|
|
|
|
setEditData({ filename, id });
|
|
|
|
|
setEditOpen(true);
|
|
|
|
|
}}
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<Separator />
|
|
|
|
|
<div className="flex items-center gap-3">
|
2025-08-29 16:18:32 +02:00
|
|
|
<UploadModal isAdmin={isAdmin} userId={userId} />{" "}
|
2025-08-28 18:27:07 +02:00
|
|
|
<span className="text-sm">Max 5 MB per file</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2 rounded-md border border-orange-500 p-2">
|
|
|
|
|
<div className="flex items-center gap-2 text-orange-500">
|
|
|
|
|
<TriangleAlert className="size-5" />
|
|
|
|
|
{t.importante}:
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm">{t.allegati.disclamer}</span>
|
|
|
|
|
</div>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
{not_from_admin && (
|
|
|
|
|
<FileList
|
2025-08-29 16:18:32 +02:00
|
|
|
delete_onClick={(id) => deleteFile({ fileId: id, userId })}
|
2025-08-28 18:27:07 +02:00
|
|
|
download_onClick={(file) =>
|
|
|
|
|
handleDownload({
|
|
|
|
|
file,
|
|
|
|
|
getToken: async () => await getToken(),
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-08-29 16:18:32 +02:00
|
|
|
files={not_from_admin}
|
|
|
|
|
isAdmin={isAdmin}
|
|
|
|
|
selectHandler={(id, filename) => {
|
|
|
|
|
setEditData({ filename, id });
|
|
|
|
|
setEditOpen(true);
|
|
|
|
|
}}
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<EditFileDialog
|
|
|
|
|
data={editData}
|
|
|
|
|
handleEditSubmit={handleEditSubmit}
|
2025-08-29 16:18:32 +02:00
|
|
|
onOpenChange={setEditOpen}
|
|
|
|
|
open={editOpen}
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const ExtIcon = ({
|
2025-08-28 18:27:07 +02:00
|
|
|
ext,
|
|
|
|
|
className,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
ext: string | null | undefined;
|
|
|
|
|
className?: string;
|
2025-08-04 17:45:44 +02:00
|
|
|
}) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
if (!ext) return <File className={cn("size-5 text-gray-500", className)} />;
|
|
|
|
|
if (ext.includes("pdf")) {
|
|
|
|
|
return <FileText className={cn("size-5 text-red-500", className)} />;
|
2025-09-01 16:48:03 +02:00
|
|
|
}
|
|
|
|
|
if (
|
2025-08-28 18:27:07 +02:00
|
|
|
ext.includes("image") ||
|
|
|
|
|
["jpg", "jpeg", "png", "gif", "svg", "webp"].some((ext) =>
|
|
|
|
|
ext.includes(ext),
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
|
2025-09-01 16:48:03 +02:00
|
|
|
}
|
|
|
|
|
if (
|
2025-08-28 18:27:07 +02:00
|
|
|
["html", "css", "js", "jsx", "ts", "tsx", "json", "xml"].some((ext) =>
|
|
|
|
|
ext.includes(ext),
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return <FileCode className={cn("size-5 text-emerald-500", className)} />;
|
2025-09-01 16:48:03 +02:00
|
|
|
}
|
|
|
|
|
if (["xls", "xlsx", "csv"].some((ext) => ext.includes(ext))) {
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<FileSpreadsheet className={cn("size-5 text-green-600", className)} />
|
|
|
|
|
);
|
2025-09-01 16:48:03 +02:00
|
|
|
}
|
|
|
|
|
if (["zip", "rar", "7z", "tar", "gz"].some((ext) => ext.includes(ext))) {
|
2025-08-28 18:27:07 +02:00
|
|
|
return <FileArchive className={cn("size-5 text-amber-600", className)} />;
|
2025-09-01 16:48:03 +02:00
|
|
|
}
|
|
|
|
|
if (["mp3", "wav", "ogg", "flac"].some((ext) => ext.includes(ext))) {
|
2025-08-28 18:27:07 +02:00
|
|
|
return <FileAudio className={cn("size-5 text-purple-500", className)} />;
|
2025-09-01 16:48:03 +02:00
|
|
|
}
|
|
|
|
|
if (["mp4", "avi", "mov", "wmv", "webm"].some((ext) => ext.includes(ext))) {
|
2025-08-28 18:27:07 +02:00
|
|
|
return <FileVideo className={cn("size-5 text-pink-500", className)} />;
|
|
|
|
|
}
|
2025-09-01 16:48:03 +02:00
|
|
|
return <File className={cn("size-5 text-gray-500", className)} />;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const EditFileDialog = ({
|
2025-08-28 18:27:07 +02:00
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
data,
|
|
|
|
|
handleEditSubmit,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (status: boolean) => void;
|
|
|
|
|
data: {
|
|
|
|
|
id: StorageindexId;
|
|
|
|
|
filename: string;
|
|
|
|
|
} | null;
|
|
|
|
|
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
2025-08-04 17:45:44 +02:00
|
|
|
}) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
return (
|
2025-08-29 16:18:32 +02:00
|
|
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
2025-08-28 18:27:07 +02:00
|
|
|
<DialogContent>
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>{t.modifica}</DialogTitle>
|
|
|
|
|
<DialogDescription className="sr-only">Edit</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
{data && (
|
2025-08-29 16:18:32 +02:00
|
|
|
<form method="post" onSubmit={handleEditSubmit}>
|
2025-08-28 18:27:07 +02:00
|
|
|
<div className="flex items-center gap-3">
|
2025-08-29 16:18:32 +02:00
|
|
|
<label className="sr-only" htmlFor="id">
|
2025-08-28 18:27:07 +02:00
|
|
|
ID
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
className="hidden"
|
|
|
|
|
name="id"
|
|
|
|
|
readOnly
|
2025-08-29 16:18:32 +02:00
|
|
|
type="text"
|
|
|
|
|
value={data.id}
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
2025-08-29 16:18:32 +02:00
|
|
|
<label className="sr-only" htmlFor="filename">
|
2025-08-28 18:27:07 +02:00
|
|
|
filename
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
className="mt-0"
|
2025-08-29 16:18:32 +02:00
|
|
|
defaultValue={data.filename || undefined}
|
2025-08-28 18:27:07 +02:00
|
|
|
id="filename"
|
|
|
|
|
name="filename"
|
2025-08-29 16:18:32 +02:00
|
|
|
placeholder="Modifica messaggio"
|
|
|
|
|
type="text"
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
<Button type="submit">{t.salva}</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
)}
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface FileListProps {
|
2025-08-28 18:27:07 +02:00
|
|
|
files: Storageindex[];
|
|
|
|
|
delete_onClick: (id: StorageindexId) => void;
|
|
|
|
|
download_onClick: (file: Storageindex) => void;
|
|
|
|
|
isAdmin: boolean;
|
|
|
|
|
selectHandler: (id: StorageindexId, filename: string) => void;
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function FileList({
|
2025-08-28 18:27:07 +02:00
|
|
|
files,
|
|
|
|
|
delete_onClick,
|
|
|
|
|
download_onClick,
|
|
|
|
|
isAdmin,
|
|
|
|
|
selectHandler,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: FileListProps) {
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<div className="w-full">
|
|
|
|
|
<div className="rounded-md border">
|
2025-10-10 16:18:43 +02:00
|
|
|
<div className="grid grid-cols-12 gap-2 bg-muted/50 p-4 font-medium text-muted-foreground text-sm">
|
2025-08-28 18:27:07 +02:00
|
|
|
<div className="col-span-6 md:col-span-8">File</div>
|
|
|
|
|
<div className="col-span-4 md:col-span-3">Data</div>
|
|
|
|
|
<div className="col-span-2 text-right md:col-span-1">Azioni</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="divide-y">
|
|
|
|
|
{files.map((file) => (
|
|
|
|
|
<div
|
2025-10-10 16:18:43 +02:00
|
|
|
className="grid grid-cols-12 items-center gap-2 p-4 text-sm transition-colors hover:bg-muted/50"
|
2025-08-29 16:18:32 +02:00
|
|
|
key={file.id}
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
<div className="col-span-6 flex items-center gap-2 md:col-span-8">
|
|
|
|
|
<div className="shrink-0">
|
|
|
|
|
<ExtIcon ext={file.ext} />
|
|
|
|
|
</div>
|
|
|
|
|
<Link
|
|
|
|
|
aria-label="Allegato"
|
2025-10-10 16:18:43 +02:00
|
|
|
className="truncate font-medium text-foreground hover:underline"
|
2025-08-28 18:27:07 +02:00
|
|
|
href={`/area-riservata/allegato-view/${file.id}`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
>
|
|
|
|
|
{file.filename}
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-10-10 16:18:43 +02:00
|
|
|
<div className="col-span-4 text-muted-foreground md:col-span-3">
|
2025-08-28 18:27:07 +02:00
|
|
|
{format(file.created_at, "d MMM yyyy")}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-span-2 flex justify-end gap-1 md:col-span-1">
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Download"
|
|
|
|
|
asChild
|
|
|
|
|
className="size-8"
|
2025-08-29 16:18:32 +02:00
|
|
|
size="icon"
|
2025-08-28 18:27:07 +02:00
|
|
|
title="Download"
|
2025-08-29 16:18:32 +02:00
|
|
|
variant="ghost"
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
className="flex gap-1 text-sm"
|
|
|
|
|
onClick={() => download_onClick(file)}
|
2025-08-29 16:18:32 +02:00
|
|
|
variant="outline"
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
<Download className="size-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</Button>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
{isAdmin && (
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
2025-08-29 16:18:32 +02:00
|
|
|
<Button className="size-8" size="icon" variant="ghost">
|
2025-08-28 18:27:07 +02:00
|
|
|
<MoreVertical className="size-4" />
|
|
|
|
|
<span className="sr-only">More options</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="end">
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
aria-label="Rinomina"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
selectHandler(file.id, file.filename || "")
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Pencil className="mr-2 size-4" />
|
|
|
|
|
Rinomina
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
aria-label="Elimina"
|
|
|
|
|
className="text-destructive focus:text-destructive"
|
2025-08-29 16:18:32 +02:00
|
|
|
onClick={() => delete_onClick(file.id)}
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
<Trash2 className="mr-2 size-4" />
|
|
|
|
|
Elimina
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|