Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const pageTitles: Record<string, string> = {
"/": "Acasă",
"/noutati": "Anunțuri",
"/facultati": "Facultăți",
"/facilitati": "Facilități",
"/harti": "Hărți",
"/sesizari": "Sesizări",
"/cantina": "Cantină",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
import {
AlertTriangle,
GraduationCap,
Building,
Home,
MapPin,
Menu,
Expand All @@ -31,6 +32,7 @@ const menuItems = [
{ label: "Acasă", href: "/", icon: Home, canAccess: canAccessDashboard },
{ label: "Anunțuri", href: "/noutati", icon: Newspaper, canAccess: canAccessContent },
{ label: "Facultăți", href: "/facultati", icon: GraduationCap, canAccess: canAccessFaculties },
{ label: "Facilități", href: "/facilitati", icon: Building, canAccess: canAccessFaculties },
{ label: "Hărți", href: "/harti", icon: MapPin, canAccess: canAccessMaps },
{ label: "Sesizări", href: "/sesizari", icon: AlertTriangle, canAccess: canAccessComplaints },
{ label: "Cantină", href: "/cantina", icon: Utensils, canAccess: canAccessCantina },
Expand Down
122 changes: 122 additions & 0 deletions Frontend/Dashboard/dashboard-insideugal/app/facilitati/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";

import React, { useState } from "react";
import Image from "next/image";
import Table, { Column } from "../components/ui/Table";
import Modal from "../components/ui/Modal";
import { useFacilities, useCreateFacility, useUpdateFacility, useDeleteFacility } from "@/hooks/useDashboardApi";
import { canAccessFaculties, useRequireDashboardAccess } from "@/lib/dashboard-auth";
import { FacilityItem } from "@/types/facility";

export default function FacilitiesPage() {
const access = useRequireDashboardAccess(canAccessFaculties);
const [activeModal, setActiveModal] = useState<"add" | "edit" | null>(null);
const [viewModal, setViewModal] = useState<FacilityItem | null>(null);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [form, setForm] = useState<Partial<FacilityItem>>({});

const { data: facilitiesData, isLoading, refetch } = useFacilities();
const createMutation = useCreateFacility();
const updateMutation = useUpdateFacility();
const deleteMutation = useDeleteFacility();

const facilities = (facilitiesData as { items: FacilityItem[] })?.items ?? [];

const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (activeModal === "edit" && selectedId) {
await updateMutation.mutateAsync({ id: selectedId, data: form });
} else {
await createMutation.mutateAsync(form);
}
setActiveModal(null);
setForm({});
refetch();
} catch (err) {
console.error("Eroare la salvare:", err);
}
};

const columns: Column<FacilityItem>[] = [
{
header: "Imagine",
key: "image_url",
render: (item: FacilityItem) => (
<div className="w-12 h-12 relative rounded-lg overflow-hidden bg-gray-50">
{item.image_url ? (
<Image src={item.image_url} alt={item.name} fill className="object-cover" sizes="48px" />
) : (
<span className="flex items-center justify-center h-full text-[10px] text-gray-400">N/A</span>
)}
</div>
),
},
{ header: "Nume", key: "name", render: (item: FacilityItem) => <span className="font-semibold cursor-pointer hover:text-blue-900" onClick={() => setViewModal(item)}>{item.name}</span> },
{ header: "Descriere", key: "description", render: (item: FacilityItem) => <span className="truncate max-w-[250px] block">{item.description || "-"}</span> },
{
header: "Acțiuni",
key: "actions",
render: (item: FacilityItem) => (
<div className="flex space-x-3 text-xs">
<button type="button" className="text-blue-600 hover:underline font-semibold" onClick={() => { setSelectedId(item.id); setForm(item); setActiveModal("edit"); }}>Editare</button>
<button type="button" className="text-red-500 hover:underline font-semibold" onClick={async () => { if(confirm("Ștergi?")) { await deleteMutation.mutateAsync(item.id); refetch(); }}}>Ștergere</button>
</div>
),
},
];

if (access.loading || !access.allowed) return null;

return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
<div className="flex justify-end items-center">
<button onClick={() => { setForm({}); setActiveModal("add"); }} className="bg-brand text-white px-5 py-2 rounded-xl text-sm font-bold">Adaugă</button>
</div>

<div className="bg-white border border-gray-200 rounded-2xl shadow-sm overflow-hidden">
{isLoading ? <div className="p-12 text-center">Se încarcă...</div> : <Table data={facilities} columns={columns} />}
</div>

<Modal isOpen={activeModal !== null} onClose={() => setActiveModal(null)} title={activeModal === "edit" ? "Editare" : "Adăugare"}>
<form onSubmit={handleSave} className="space-y-4">
<input value={form.name || ""} onChange={e => setForm({...form, name: e.target.value})} className="w-full bg-gray-100 p-3 rounded-lg outline-none" placeholder="Nume" required />
<input value={form.image_url || ""} onChange={e => setForm({...form, image_url: e.target.value})} className="w-full bg-gray-100 p-3 rounded-lg outline-none" placeholder="URL Imagine" />
<textarea value={form.description || ""} onChange={e => setForm({...form, description: e.target.value})} className="w-full bg-gray-100 p-3 rounded-lg outline-none" placeholder="Descriere" />
<button type="submit" className="w-full bg-brand text-white py-3 rounded-lg font-bold">Salvează</button>
</form>
</Modal>

<Modal
isOpen={viewModal !== null}
onClose={() => setViewModal(null)}
title={viewModal?.name ?? ""}
>
{viewModal && (
<div className="space-y-5">
{viewModal.image_url && (
<div className="relative w-full h-64 rounded-xl overflow-hidden">
<Image
src={viewModal.image_url}
alt={viewModal.name}
fill
className="object-cover"
/>
</div>
)}

<div>
<h2 className="text-xl font-bold">
{viewModal.name}
</h2>

<p className="mt-3 whitespace-pre-wrap text-gray-700 leading-relaxed">
{viewModal.description || "Nu există descriere."}
</p>
</div>
</div>
)}
</Modal>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useQuery } from "@tanstack/react-query";
import { z } from "zod"; // Adăugat pentru validarea schemei generice

import { apiClient } from "@/lib/api-client";
import { useApiMutation } from "@/hooks/useApiMutation";
import { useApiQuery } from "@/hooks/useApiQuery";
import { announcementsService } from "@/lib/announcements-service";
Expand Down Expand Up @@ -88,4 +88,37 @@ export function useComplaints() {
queryKey: ["complaints"],
schema: complaintsGenericSchema, // Schema pasată obligatoriu pentru a rezolva eroarea TS2345
});
}

import type { FacilityItem } from "@/types/facility";

export function useFacilities() {
return useQuery({
queryKey: ["facilities"],
queryFn: () => apiClient.getFacilities(),
});
}

export function useCreateFacility() {
return useApiMutation<FacilityItem, Partial<FacilityItem>>({
mutationFn: (data) => apiClient.createFacility(data),
invalidateKeys: [["facilities"]],
});
}

export function useUpdateFacility() {
return useApiMutation<
FacilityItem,
{ id: number; data: Partial<FacilityItem> }
>({
mutationFn: ({ id, data }) => apiClient.updateFacility(id, data),
invalidateKeys: [["facilities"]],
});
}

export function useDeleteFacility() {
return useApiMutation<unknown, number>({
mutationFn: (id) => apiClient.deleteFacility(id),
invalidateKeys: [["facilities"]],
});
}
49 changes: 38 additions & 11 deletions Frontend/Dashboard/dashboard-insideugal/lib/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { z } from "zod";

import {
announcementSchema,
announcementsSchema,
coursesSchema,
facultiesSchema,
facultySchema,
notificationSchema,
notificationsSchema,
profileSchema,
userSchema,
announcementSchema,
announcementsSchema,
coursesSchema,
facultiesSchema,
facilitySchema,
facilitiesSchema,
notificationSchema,
notificationsSchema,
profileSchema,
userSchema,
} from "./api-schemas";
import type { Announcement, ApiErrorBody, ApiRequestOptions } from "./api-types";

import { FacilityItem } from "../types/facility"; // Ajustează calea dacă e nevoie
import { facultySchema } from "./api-schemas";

// 👉 REPARAT: Forțăm http în mod explicit pe local pentru a preveni ERR_SSL_PROTOCOL_ERROR
// Chiar dacă în .env ai din greșeală "https", codul de mai jos se va asigura că rămâne "http" pe localhost.
const rawUrl = process.env.NEXT_PUBLIC_BACKEND_URL?.replace(/\/$/, "") ?? "http://localhost:8002";
Expand Down Expand Up @@ -380,9 +384,32 @@ export const apiClient = {
apiRequest(`/announcements/${id}`, z.unknown(), {
method: "DELETE",
}),
getCourses: () => apiRequest("/courses", coursesSchema),
getFaculties: () => apiRequest("/faculties", facultiesSchema),

getFaculties: () => apiRequest("/faculties", facultiesSchema),
getFaculty: (id: number) => apiRequest(`/faculties/${id}`, facultySchema),

getFacilities: () =>
apiRequest("/facilities", facilitiesSchema),

createFacility: (data: Partial<FacilityItem>) =>
apiRequest("/facilities/", facilitySchema, {
method: "POST",
body: data,
}),

updateFacility: (id: number, data: Partial<FacilityItem>) =>
apiRequest(`/facilities/${id}`, facilitySchema, {
method: "PATCH",
body: data,
}),

deleteFacility: (id: number) =>
apiRequest(`/facilities/${id}`, z.unknown(), {
method: "DELETE",
}),

// Restul metodelor (getCourses, getNotifications etc.) rămân mai jos...
getCourses: () => apiRequest("/courses", coursesSchema),
getCurrentUser: () => apiRequest("/users/me", userSchema),
getCurrentProfile: () => apiRequest("/profiles/me", profileSchema),
getNotifications: (params?: { faculty_id?: number; page?: number; size?: number }) => {
Expand Down
10 changes: 10 additions & 0 deletions Frontend/Dashboard/dashboard-insideugal/lib/api-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,13 @@ export const profileSchema = z.object({
updated_at: isoDateSchema,
});

export const facilitySchema = z.object({
id: z.number().int(),
name: z.string(),
description: z.string().nullable().optional(),
image_url: z.string().nullable().optional(),
created_at: isoDateSchema,
updated_at: isoDateSchema,
});

export const facilitiesSchema = createPaginatedResponseSchema(facilitySchema);
8 changes: 8 additions & 0 deletions Frontend/Dashboard/dashboard-insideugal/types/facility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type FacilityItem = {
id: number;
name: string;
description?: string | null;
image_url?: string | null;
created_at: string;
updated_at: string;
};
Loading