Commit baa26615 authored by Anthony Jacob's avatar Anthony Jacob
Browse files

add drag and drop, work on Keyword and diploma

parent 3af90502
Loading
Loading
Loading
Loading
+70 −0
Original line number Diff line number Diff line
@@ -8,6 +8,9 @@
      "name": "back-nextjs",
      "version": "0.1.0",
      "dependencies": {
        "@dnd-kit/core": "^6.3.1",
        "@dnd-kit/modifiers": "^9.0.0",
        "@dnd-kit/sortable": "^10.0.0",
        "@types/jsonwebtoken": "^9.0.9",
        "bootstrap": "^5.3.6",
        "bootstrap-icons": "^1.13.1",
@@ -66,6 +69,73 @@
        "node": ">=6.9.0"
      }
    },
    "node_modules/@dnd-kit/accessibility": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
      "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
      "license": "MIT",
      "dependencies": {
        "tslib": "^2.0.0"
      },
      "peerDependencies": {
        "react": ">=16.8.0"
      }
    },
    "node_modules/@dnd-kit/core": {
      "version": "6.3.1",
      "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
      "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
      "license": "MIT",
      "dependencies": {
        "@dnd-kit/accessibility": "^3.1.1",
        "@dnd-kit/utilities": "^3.2.2",
        "tslib": "^2.0.0"
      },
      "peerDependencies": {
        "react": ">=16.8.0",
        "react-dom": ">=16.8.0"
      }
    },
    "node_modules/@dnd-kit/modifiers": {
      "version": "9.0.0",
      "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
      "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
      "license": "MIT",
      "dependencies": {
        "@dnd-kit/utilities": "^3.2.2",
        "tslib": "^2.0.0"
      },
      "peerDependencies": {
        "@dnd-kit/core": "^6.3.0",
        "react": ">=16.8.0"
      }
    },
    "node_modules/@dnd-kit/sortable": {
      "version": "10.0.0",
      "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
      "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
      "license": "MIT",
      "dependencies": {
        "@dnd-kit/utilities": "^3.2.2",
        "tslib": "^2.0.0"
      },
      "peerDependencies": {
        "@dnd-kit/core": "^6.3.0",
        "react": ">=16.8.0"
      }
    },
    "node_modules/@dnd-kit/utilities": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
      "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
      "license": "MIT",
      "dependencies": {
        "tslib": "^2.0.0"
      },
      "peerDependencies": {
        "react": ">=16.8.0"
      }
    },
    "node_modules/@emnapi/core": {
      "version": "1.4.3",
      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
+3 −0
Original line number Diff line number Diff line
@@ -9,6 +9,9 @@
    "lint": "next lint"
  },
  "dependencies": {
    "@dnd-kit/core": "^6.3.1",
    "@dnd-kit/modifiers": "^9.0.0",
    "@dnd-kit/sortable": "^10.0.0",
    "@types/jsonwebtoken": "^9.0.9",
    "bootstrap": "^5.3.6",
    "bootstrap-icons": "^1.13.1",
+1 −1
Original line number Diff line number Diff line
@@ -107,7 +107,7 @@ export default function ApiKeyListClient({ data }: { data: any }) {
    if (!Array.isArray(ApiKeys) || ApiKeys.length === 0) {
        return (
            <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 apikey-list-container">
                <h2 className="text-2xl font-semibold mb-4">User</h2>
                <h2 className="text-2xl font-semibold mb-4">Api Key</h2>
                <div>No Api Key found.</div>
            </div>
        );
+2 −2
Original line number Diff line number Diff line
@@ -37,8 +37,8 @@ export default function ApiKeyEditPageClient({ data }: { data: { id: number, api
                const { id, api_key } = await saveNewAPIKey(label, valid_until, is_valid);
                setNewAPIKEY(api_key);
                setApiKeyID(id);
                /*const id = await saveNewUser(login, password, is_active, force_jwt_reconnect);
                setTimeout(() => router.push(`/Admin/User/edit/${id}`), 1000);*/

                /*setTimeout(() => router.push(`/Admin/User/edit/${id}`), 1000);*/
            }
            else {
                await saveAPIKey(ApiKeyID, label, valid_until, is_valid);
+168 −0
Original line number Diff line number Diff line
'use client';
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { deleteDiploma } from "./actions";


export default function DiplomaListClient({ data, enabledLanguage }:
    { data: any, enabledLanguage: any }) {
    const router = useRouter();
    const Diplomas = Array.isArray(data?.diplomas) ? data.diplomas : [];

    const [selectedDiplomas, setselectedDiplomas] = useState<Record<number, boolean>>({});
    const handleSelect = (id: number, checked: boolean) => {
        setselectedDiplomas((prev) => ({ ...prev, [id]: checked }));
    };
    const [showDeleteModal, setShowDeleteModal] = useState(false);
    // Toasts state for multiple simultaneous toasts
    const [toasts, setToasts] = useState<Array<{ id: number; message: string; type: 'success' | 'danger' }>>([]);

    const confirmDelete = async () => {
        const DiplomaIds = Object.keys(selectedDiplomas).filter(id => selectedDiplomas[Number(id)]);
        if (DiplomaIds.length === 0) {
            console.warn("No Diplomas selected for deletion.");
            return;
        }
        setShowDeleteModal(false);
        console.log("Confirmed deletion for Diploma IDs:", DiplomaIds);

        try {
            await Promise.all(DiplomaIds.map(id => deleteDiploma(Number(id))));
            setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: 'Selected Diploma deleted successfully.', type: 'success' }]);

            setselectedDiplomas({});
            router.refresh();
        } catch (err) {
            setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: 'Failed to delete selected Diploma.', type: 'danger' }]);
            setselectedDiplomas({});
        }

    };


    const handleDelete = async () => {
        const DiplomaIds = Object.keys(selectedDiplomas).filter(id => selectedDiplomas[Number(id)]);
        if (DiplomaIds.length === 0) {
            console.warn("No Diplomas selected for deletion.");
            return;
        }
        setShowDeleteModal(true);
        console.log("Selected Diploma IDs for deletion:", DiplomaIds);

    };


    // Auto-hide each toast after 3 seconds
    useEffect(() => {
        if (toasts.length > 0) {
            const timers = toasts.map((toast) =>
                setTimeout(() => {
                    setToasts(ts => ts.filter(t => t.id !== toast.id));
                }, 3000)
            );
            return () => timers.forEach(timer => clearTimeout(timer));
        }
    }, [toasts]);

    if (!Array.isArray(Diplomas) || Diplomas.length === 0) {
        return (
            <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 diploma-list-container">
                <h2 className="text-2xl font-semibold mb-4">Diploma</h2>
                <div>No Diploma found.</div>
            </div>
        );
    }
    console.log("Diplomas:", Diplomas);

    return (
        <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 diploma-list-container">

            <h2 className="text-2xl font-semibold mb-4">Diploma and training List</h2>
            <table className="table table-striped">
                <thead>
                    <tr>
                        <th></th>
                        <th>ID</th>
                        <th>Title</th>
                        <th>Edit</th>
                    </tr>
                </thead>
                <tbody>
                    {Diplomas && Diplomas.map((diploma: any) => (
                        <tr key={diploma.id}>
                            <td>
                                <input
                                    type="checkbox"
                                    className="form-check-input"
                                    checked={!!selectedDiplomas[diploma.id]}
                                    onChange={e => handleSelect(diploma.id, e.target.checked)}
                                />
                            </td>
                            <td>{diploma.id}</td>
                            <td>{
                                diploma.translations.find((t: any) => t.language_code === 'fr')?.title ||
                                diploma.translations[0]?.title || ''
                            }</td>
                            <td>
                                <a href={`/Admin/Diploma/edit/${diploma.id}`} className="btn btn-primary">
                                    Edit
                                </a>
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
            {/* Action buttons at the bottom of the grid */}
            <div className="d-flex justify-content-end gap-2 mt-3">
                <button
                    className="btn btn-danger"
                    style={{ borderRadius: "50%", width: 56, height: 56, fontSize: 24 }}
                    title="Delete"
                    onClick={handleDelete}
                    disabled={Object.keys(selectedDiplomas).length === 0}
                >
                    <i className="bi bi-trash"></i>
                </button>
                <a href="/Admin/Diploma/edit/new" className="btn btn-primary" style={{ borderRadius: "50%", width: 56, height: 56, fontSize: 28 }} title="Add">
                    <i className="bi bi-plus"></i>
                </a>
            </div>
            {/* Delete Confirmation Modal */}
            <div className={`modal fade${showDeleteModal ? ' show d-block' : ''}`} tabIndex={-1} role="dialog" style={{ background: showDeleteModal ? 'rgba(0,0,0,0.5)' : undefined }}>
                <div className="modal-dialog" role="document">
                    <div className="modal-content">
                        <div className="modal-header">
                            <h5 className="modal-title">Confirm Delete</h5>
                            <button type="button" className="btn-close" aria-label="Close" onClick={() => setShowDeleteModal(false)}></button>
                        </div>
                        <div className="modal-body">
                            <p>Are you sure you want to delete {Object.values(selectedDiplomas).map(v => !v).length} Diploma(s)? This action cannot be undone.</p>
                        </div>
                        <div className="modal-footer">
                            <button type="button" className="btn btn-secondary" onClick={() => setShowDeleteModal(false)}>Cancel</button>
                            <button type="button" className="btn btn-danger" onClick={confirmDelete}>Delete</button>
                        </div>
                    </div>
                </div>
            </div>
            {/* Toast messages (multiple, simultaneous) */}
            <div style={{ position: 'fixed', top: 20, right: 20, zIndex: 2000 }}>
                {toasts.map((toast) => (
                    <div
                        key={toast.id}
                        className={`toast align-items-center text-bg-${toast.type} border-0 mb-2 show`}
                        role="alert"
                        aria-live="assertive"
                        aria-atomic="true"
                        style={{ minWidth: 250 }}
                    >
                        <div className="d-flex">
                            <div className="toast-body">{toast.message}</div>
                            <button type="button" className="btn-close btn-close-white me-2 m-auto" aria-label="Close" onClick={() => setToasts(ts => ts.filter(t => t.id !== toast.id))}></button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
}
 No newline at end of file
Loading