From cd020e3eb8f8aba01c54de808e8682df04e972e1 Mon Sep 17 00:00:00 2001 From: matej Date: Sat, 6 Sep 2025 14:27:54 +0200 Subject: [PATCH] Redesigned UI Fixed SQL query issues with server --- client/package-lock.json | 23 +- client/package.json | 4 +- client/src/App.jsx | 1839 ++++++++++++++--------- client/src/components/Login.jsx | 203 +-- client/src/components/ui/color-mode.jsx | 90 ++ client/src/components/ui/provider.jsx | 12 + client/src/components/ui/toaster.jsx | 43 + client/src/components/ui/tooltip.jsx | 35 + client/src/index.css | 23 +- client/src/main.jsx | 7 +- client/src/tsconfig.app.json | 11 + server/server/index.js | 1250 ++++++++------- 12 files changed, 2154 insertions(+), 1386 deletions(-) create mode 100644 client/src/components/ui/color-mode.jsx create mode 100644 client/src/components/ui/provider.jsx create mode 100644 client/src/components/ui/toaster.jsx create mode 100644 client/src/components/ui/tooltip.jsx create mode 100644 client/src/tsconfig.app.json diff --git a/client/package-lock.json b/client/package-lock.json index 009db09..4d114bf 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,8 +13,10 @@ "@emotion/styled": "^11.14.1", "firebase": "^12.2.1", "framer-motion": "^12.23.12", + "next-themes": "^0.4.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-icons": "^5.5.0" }, "devDependencies": { "@eslint/js": "^9.19.0", @@ -5569,6 +5571,16 @@ "dev": true, "license": "MIT" }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -5959,6 +5971,15 @@ "react": "^19.0.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/client/package.json b/client/package.json index 1803fde..00ecc98 100644 --- a/client/package.json +++ b/client/package.json @@ -15,8 +15,10 @@ "@emotion/styled": "^11.14.1", "firebase": "^12.2.1", "framer-motion": "^12.23.12", + "next-themes": "^0.4.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-icons": "^5.5.0" }, "devDependencies": { "@eslint/js": "^9.19.0", diff --git a/client/src/App.jsx b/client/src/App.jsx index cf179dc..7999065 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,8 +1,12 @@ import React, {useState, useEffect} from 'react'; +import { Box, Button, Flex, Heading, Input, VStack, HStack, Text, IconButton } from '@chakra-ui/react'; +import { HiMoon, HiSun } from 'react-icons/hi'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import Login from './components/Login'; -const endpoint = `http://localhost:3001` +// Local Development + const endpoint = `http://localhost:3001` +// const endpoint = `https://urejevalnik.dztps.si` // Jezik (Language) Components const JezikForm = ({jezik, onSubmit, onCancel}) => { @@ -15,48 +19,58 @@ const JezikForm = ({jezik, onSubmit, onCancel}) => { }; return ( -
-
- -
-
- - -
-
+ +
+ + + Jezik: + setFormData({...formData, jezik: e.target.value})} + required + /> + + + + + + +
+
); }; const JezikList = ({jeziki, onEdit, onDelete}) => ( - - - - - - - - - - {jeziki.map(jezik => ( - - - - - - ))} - -
IDLanguageActions
{jezik.id}{jezik.jezik} - - -
+ + + + + + + + + + + {jeziki.map(jezik => ( + + + + + + ))} + +
IDLanguageActions
{jezik.id}{jezik.jezik} + + + + +
+
); // Podrocje (Field) Components @@ -69,48 +83,59 @@ const PodrocjeForm = ({podrocje, onSubmit, onCancel}) => { }; return ( -
-
- -
-
- - -
-
+ +
+ + + Področje: + setFormData({...formData, podrocje: e.target.value})} + required + placeholder="Enter field name" + /> + + + + + + +
+
); }; const PodrocjeList = ({podrocja, onEdit, onDelete}) => ( - - - - - - - - - - {podrocja.map(podrocje => ( - - - - - - ))} - -
IDFieldActions
{podrocje.id}{podrocje.podrocje} - - -
+ + + + + + + + + + + {podrocja.map(podrocje => ( + + + + + + ))} + +
IDFieldActions
{podrocje.id}{podrocje.podrocje} + + + + +
+
); // Vloga (Role) Components @@ -127,74 +152,83 @@ const VlogaForm = ({vloga, onSubmit, onCancel}) => { }; return ( -
-
- -
-
- -
-
- -
-
- - -
-
+ +
+ + + Vloga: + setFormData({...formData, vloga: e.target.value})} + required + placeholder="Enter role name" + /> + + + Izpis (ženski): + setFormData({...formData, izpis_zenska: e.target.value})} + required + placeholder="Enter female form" + /> + + + Izpis (moški): + setFormData({...formData, izpis_moski: e.target.value})} + required + placeholder="Enter male form" + /> + + + + + + +
+
); }; const VlogaList = ({vloge, onEdit, onDelete}) => ( - - - - - - - - - - - - {vloge.map(vloga => ( - - - - - - - - ))} - -
IDRoleFemale FormMale FormActions
{vloga.id}{vloga.vloga}{vloga.izpis_zenska}{vloga.izpis_moski} - - -
+ + + + + + + + + + + + + {vloge.map(vloga => ( + + + + + + + + ))} + +
IDRoleFemale FormMale FormActions
{vloga.id}{vloga.vloga}{vloga.izpis_zenska}{vloga.izpis_moski} + + + + +
+
); // Protected App Component (requires authentication) @@ -206,6 +240,9 @@ const ProtectedApp = () => { const [members, setMembers] = useState([]); const [editingMember, setEditingMember] = useState(null); const [showMemberForm, setShowMemberForm] = useState(false); + const [viewingMember, setViewingMember] = useState(null); + const [showMemberDetailsModal, setShowMemberDetailsModal] = useState(false); + const [memberTranslations, setMemberTranslations] = useState([]); // Jezik state const [jeziki, setJeziki] = useState([]); @@ -393,148 +430,222 @@ const ProtectedApp = () => { } }; + const handleViewMemberDetails = async (member) => { + setViewingMember(member); + try { + const response = await fetch(`${endpoint}/express/api/v1/clan/${member.id}/prevod/`, { + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + } + }); + const data = await response.json(); + setMemberTranslations(data); + } catch (error) { + console.error('Error fetching member translations:', error); + setMemberTranslations([]); + } + setShowMemberDetailsModal(true); + }; + + const handleCloseMemberDetailsModal = () => { + setShowMemberDetailsModal(false); + setViewingMember(null); + setMemberTranslations([]); + }; + return ( -
-
-

DZTPS Management

-
- Welcome, {currentUser?.email} - -
-
+ + + -
-
- - - - -
+ + + + + + + + - {activeTab === 'members' && ( -
-

Members Management

- - {showMemberForm ? ( - handleSubmit('members', formData)} - onCancel={() => { - setEditingMember(null); - setShowMemberForm(false); - }} - /> - ) : ( - { - setEditingMember(member); - setShowMemberForm(true); - }} - onDelete={(id) => handleDelete('members', id)} - /> + {activeTab === 'members' && ( + + + Members Management + + + {showMemberForm ? ( + handleSubmit('members', formData)} + onCancel={() => { + setEditingMember(null); + setShowMemberForm(false); + }} + /> + ) : ( + { + setEditingMember(member); + setShowMemberForm(true); + }} + onDelete={(id) => handleDelete('members', id)} + onViewDetails={handleViewMemberDetails} + /> + )} + )} -
- )} - {activeTab === 'jezik' && ( -
-

Languages Management

- - {showJezikForm ? ( - handleSubmit('jezik', formData)} - onCancel={() => { - setEditingJezik(null); - setShowJezikForm(false); - }} - /> - ) : ( - { - setEditingJezik(jezik); - setShowJezikForm(true); - }} - onDelete={(id) => handleDelete('jezik', id)} - /> + {activeTab === 'jezik' && ( + + + Languages Management + + + {showJezikForm ? ( + handleSubmit('jezik', formData)} + onCancel={() => { + setEditingJezik(null); + setShowJezikForm(false); + }} + /> + ) : ( + { + setEditingJezik(jezik); + setShowJezikForm(true); + }} + onDelete={(id) => handleDelete('jezik', id)} + /> + )} + )} -
- )} - {activeTab === 'podrocje' && ( -
-

Fields Management

- - {showPodrocjeForm ? ( - handleSubmit('podrocje', formData)} - onCancel={() => { - setEditingPodrocje(null); - setShowPodrocjeForm(false); - }} - /> - ) : ( - { - setEditingPodrocje(podrocje); - setShowPodrocjeForm(true); - }} - onDelete={(id) => handleDelete('podrocje', id)} - /> + {activeTab === 'podrocje' && ( + + + Fields Management + + + {showPodrocjeForm ? ( + handleSubmit('podrocje', formData)} + onCancel={() => { + setEditingPodrocje(null); + setShowPodrocjeForm(false); + }} + /> + ) : ( + { + setEditingPodrocje(podrocje); + setShowPodrocjeForm(true); + }} + onDelete={(id) => handleDelete('podrocje', id)} + /> + )} + )} -
- )} - {activeTab === 'vloga' && ( -
-

Roles Management

- - {showVlogaForm ? ( - handleSubmit('vloga', formData)} - onCancel={() => { - setEditingVloga(null); - setShowVlogaForm(false); - }} - /> - ) : ( - { - setEditingVloga(vloga); - setShowVlogaForm(true); - }} - onDelete={(id) => handleDelete('vloga', id)} - /> + {activeTab === 'vloga' && ( + + + Roles Management + + + {showVlogaForm ? ( + handleSubmit('vloga', formData)} + onCancel={() => { + setEditingVloga(null); + setShowVlogaForm(false); + }} + /> + ) : ( + { + setEditingVloga(vloga); + setShowVlogaForm(true); + }} + onDelete={(id) => handleDelete('vloga', id)} + /> + )} + )} -
- )} -
-
+ + + + + ); }; @@ -554,131 +665,456 @@ const TranslationForm = ({translation, languages, fields, roles, onSubmit, onCan }; return ( -
-
- -
-
- -
-
- -
-
- -
-
- - {fields.map(field => ( -
- -
- ))} -
-
- - {roles.map(role => ( -
- -
- ))} -
-
- - -
-
+ Licenca DZTPS + + + + + + Fields: + + {fields.map(field => ( + + p.id === field.id)} + onChange={(e) => { + const newPodrocja = e.target.checked + ? [...formData.podrocja, field] + : formData.podrocja.filter(p => p.id !== field.id); + setFormData({...formData, podrocja: newPodrocja}); + }} + style={{ + width: '16px', + height: '16px', + accentColor: '#3182ce' + }} + /> + {field.podrocje} + + ))} + + + + + Roles: + + {roles.map(role => ( + + v.id === role.id)} + onChange={(e) => { + const newVloge = e.target.checked + ? [...formData.vloge, role] + : formData.vloge.filter(v => v.id !== role.id); + setFormData({...formData, vloge: newVloge}); + }} + style={{ + width: '16px', + height: '16px', + accentColor: '#3182ce' + }} + /> + {role.vloga} + + ))} + + + + + + + + + + + + ); +}; + +const MemberDetailsDrawer = ({member, translations, isOpen, onClose}) => { + const formatDate = (dateString) => { + if (!dateString) return '-'; + return new Date(dateString).toLocaleDateString(); + }; + + const formatPhone = (phone) => { + return phone || '-'; + }; + + if (!isOpen) return null; + + return ( + + e.stopPropagation()} + overflowY="auto" + > + + {member?.ime} {member?.priimek} + + × + + + + + + + + Basic Information + + + + ID: + {member?.id_izkaznica || '-'} + + + Gender: + {member?.spol === 'moski' ? 'Male' : member?.spol === 'zenska' ? 'Female' : '-'} + + + Birth Date: + {formatDate(member?.datum_rojstva)} + + + Birth Place: + {member?.kraj_rojstva || '-'} + + + Nationality: + {member?.drzavljanstvo || '-'} + + + Native Language: + {member?.materni_jezik || '-'} + + + + + + + Contact Information + + + + Email: + {member?.email || '-'} + + + Home Phone: + {formatPhone(member?.telefondoma)} + + + Work Phone: + {formatPhone(member?.telefonsluzba)} + + + Mobile Phone: + {formatPhone(member?.telefonmobi)} + + + Website: + {member?.spletnastran || '-'} + + + + + + + + + Address + + + + Address: + {member?.naslovbivalisca || '-'} + + + Street: + {member?.ulica || '-'} + + + Postal Code: + {member?.postna_stevilka || '-'} + + + Post Office: + {member?.posta || '-'} + + + + + + + Professional Information + + + + Education: + {member?.solska_izobrazba || '-'} + + + Basic Profession: + {member?.osnovni_poklic || '-'} + + + Current Employment: + {member?.sedanja_zaposlitev || '-'} + + + Translation Experience: + {member?.prevajalska_praksa || '-'} + + + Join Date: + {formatDate(member?.datum_vclanitve)} + + + Directory Listed: + + {member?.objava_v_iskalniku ? 'Yes' : 'No'} + + + + + + + {member?.nacin_pridobivanja_znanja_tujih_jezikov && ( + + + Language Knowledge Acquisition + + {member.nacin_pridobivanja_znanja_tujih_jezikov} + + )} + + {translations && translations.length > 0 && ( + + + Translation Skills ({translations.length}) + + + + {translations.map(translation => ( + + + + {translation.iz} → {translation.v} + + + {translation.licenca_DZTPS ? 'Licensed' : 'Not Licensed'} + + + {translation.rang && ( + Rank: {translation.rang} + )} + + {translation.podrocja && translation.podrocja.length > 0 && ( + + Fields: + + {translation.podrocja.map(field => ( + + {field.podrocje} + + ))} + + + )} + {translation.vloge && translation.vloge.length > 0 && ( + + Roles: + + {translation.vloge.map(role => ( + + {role.vloga} + + ))} + + + )} + + + ))} + + + + )} + + + + ); }; const TranslationsList = ({translations, onEdit, onDelete}) => ( - - - - - - - - - - - - {translations.map(translation => ( - - - - - - - - ))} - -
FromToRankLicenseActions
{translation.iz}{translation.v}{translation.rang}{translation.licenca_DZTPS ? 'Yes' : 'No'} - - -
+ + + + + + + + + + + + + + + {translations.map(translation => ( + + + + + + + + + + ))} + +
FromToRankLicenseFieldsRolesActions
{translation.iz}{translation.v}{translation.rang || '-'} + + {translation.licenca_DZTPS ? 'Yes' : 'No'} + + + {translation.podrocja && translation.podrocja.length > 0 ? ( + + {translation.podrocja.map(field => ( + + {field.podrocje} + + ))} + + ) : ( + - + )} + + {translation.vloge && translation.vloge.length > 0 ? ( + + {translation.vloge.map(role => ( + + {role.vloga} + + ))} + + ) : ( + - + )} + + + + + +
+
); const MemberForm = ({member, onSubmit, onCancel}) => { @@ -845,286 +1281,303 @@ const MemberForm = ({member, onSubmit, onCancel}) => { }; return ( -
- +
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - -
+ + + + ID Izkaznica: + + + + Ime: + + + + Priimek: + + + + Spol: + + + + + + Datum rojstva: + + + + Kraj rojstva: + + + + Državljanstvo: + + + + Šolska izobrazba: + + + + + + Telefon (doma): + + + + Telefon (služba): + + + + Mobilni telefon: + + + + Email: + + + + + + Spletna stran: + + + + + + Naslov bivališča: + + + + Ulica: + + + + Poštna številka: + + + + Pošta: + + + + + + Osnovni poklic: + + + + Sedanja zaposlitev: + + + + Prevajalska praksa: + + + + Datum včlanitve: + + + + + + Materni jezik: + + + + Objava v iskalniku: + + + Publish in directory + + + + + + Način pridobivanja znanja tujih jezikov: + + + + + + + +
- { - member && ( -
-

Translations

- + {member && ( + + + + Member Translations + + {showTranslationForm ? ( { onDelete={handleTranslationDelete} /> )} -
- ) - } -
+ + + )} + ) } -const MembersList = ({members, onEdit, onDelete}) => { +const MembersList = ({members, onEdit, onDelete, onViewDetails}) => { return ( -
-

Members List

- + +
- - - - - - - + + + + + + + - {members.map(member => ( - - - - - - - - ))} + {members.map(member => ( + + + + + + + + ))}
IDNameSurnameEmailActions
IDNameSurnameEmailActions
{member.id_izkaznica}{member.ime}{member.priimek}{member.email} - - -
{member.id_izkaznica}{member.ime}{member.priimek}{member.email} + + + + + +
-
+ ); }; diff --git a/client/src/components/Login.jsx b/client/src/components/Login.jsx index 630eb6c..076b521 100644 --- a/client/src/components/Login.jsx +++ b/client/src/components/Login.jsx @@ -1,4 +1,14 @@ import React, { useState } from 'react'; +import { + Box, + Button, + VStack, + HStack, + Input, + Text, + Heading, + Flex +} from '@chakra-ui/react'; import { signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'; import { auth, googleProvider } from '../firebaseConfig'; @@ -36,100 +46,107 @@ const Login = () => { }; return ( -
-

Login to DZTPS

- - {error && ( -
- {error} -
- )} - -
-
- - setEmail(e.target.value)} - required - disabled={loading} - style={{ - width: '100%', - padding: '8px', - marginTop: '5px', - border: '1px solid #ccc', - borderRadius: '4px' - }} - /> -
- -
- - setPassword(e.target.value)} - required - disabled={loading} - style={{ - width: '100%', - padding: '8px', - marginTop: '5px', - border: '1px solid #ccc', - borderRadius: '4px' - }} - /> -
- - -
- -
- OR -
- - -
+ + + Login to DZTPS + + + {error && ( + + {error} + + )} + + + + + Email + setEmail(e.target.value)} + disabled={loading} + placeholder="Enter your email" + bg="white" + required + /> + + + + Password + setPassword(e.target.value)} + disabled={loading} + placeholder="Enter your password" + bg="white" + required + /> + + + + + + + + + + OR + + + + + + + + ); }; diff --git a/client/src/components/ui/color-mode.jsx b/client/src/components/ui/color-mode.jsx new file mode 100644 index 0000000..b2d1cd6 --- /dev/null +++ b/client/src/components/ui/color-mode.jsx @@ -0,0 +1,90 @@ +'use client' + +import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react' +import { ThemeProvider, useTheme } from 'next-themes' + +import * as React from 'react' +import { LuMoon, LuSun } from 'react-icons/lu' + +export function ColorModeProvider(props) { + return ( + + ) +} + +export function useColorMode() { + const { resolvedTheme, setTheme, forcedTheme } = useTheme() + const colorMode = forcedTheme || resolvedTheme + const toggleColorMode = () => { + setTheme(resolvedTheme === 'dark' ? 'light' : 'dark') + } + return { + colorMode: colorMode, + setColorMode: setTheme, + toggleColorMode, + } +} + +export function useColorModeValue(light, dark) { + const { colorMode } = useColorMode() + return colorMode === 'dark' ? dark : light +} + +export function ColorModeIcon() { + const { colorMode } = useColorMode() + return colorMode === 'dark' ? : +} + +export const ColorModeButton = React.forwardRef( + function ColorModeButton(props, ref) { + const { toggleColorMode } = useColorMode() + return ( + }> + + + + + ) + }, +) + +export const LightMode = React.forwardRef(function LightMode(props, ref) { + return ( + + ) +}) + +export const DarkMode = React.forwardRef(function DarkMode(props, ref) { + return ( + + ) +}) diff --git a/client/src/components/ui/provider.jsx b/client/src/components/ui/provider.jsx new file mode 100644 index 0000000..80e3e01 --- /dev/null +++ b/client/src/components/ui/provider.jsx @@ -0,0 +1,12 @@ +'use client' + +import { ChakraProvider, defaultSystem } from '@chakra-ui/react' +import { ColorModeProvider } from './color-mode' + +export function Provider(props) { + return ( + + + + ) +} diff --git a/client/src/components/ui/toaster.jsx b/client/src/components/ui/toaster.jsx new file mode 100644 index 0000000..6af2b70 --- /dev/null +++ b/client/src/components/ui/toaster.jsx @@ -0,0 +1,43 @@ +'use client' + +import { + Toaster as ChakraToaster, + Portal, + Spinner, + Stack, + Toast, + createToaster, +} from '@chakra-ui/react' + +export const toaster = createToaster({ + placement: 'bottom-end', + pauseOnPageIdle: true, +}) + +export const Toaster = () => { + return ( + + + {(toast) => ( + + {toast.type === 'loading' ? ( + + ) : ( + + )} + + {toast.title && {toast.title}} + {toast.description && ( + {toast.description} + )} + + {toast.action && ( + {toast.action.label} + )} + {toast.closable && } + + )} + + + ) +} diff --git a/client/src/components/ui/tooltip.jsx b/client/src/components/ui/tooltip.jsx new file mode 100644 index 0000000..ab225cc --- /dev/null +++ b/client/src/components/ui/tooltip.jsx @@ -0,0 +1,35 @@ +import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react' +import * as React from 'react' + +export const Tooltip = React.forwardRef(function Tooltip(props, ref) { + const { + showArrow, + children, + disabled, + portalled = true, + content, + contentProps, + portalRef, + ...rest + } = props + + if (disabled) return children + + return ( + + {children} + + + + {showArrow && ( + + + + )} + {content} + + + + + ) +}) diff --git a/client/src/index.css b/client/src/index.css index 6119ad9..62310ea 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -3,9 +3,9 @@ line-height: 1.5; font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + color-scheme: light; + color: #213547; + background-color: #ffffff; font-synthesis: none; text-rendering: optimizeLegibility; @@ -24,8 +24,6 @@ a:hover { body { margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } @@ -42,7 +40,8 @@ button { font-size: 1em; font-weight: 500; font-family: inherit; - background-color: #1a1a1a; + background-color: #f9f9f9; + color: #213547; cursor: pointer; transition: border-color 0.25s; } @@ -54,15 +53,3 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/client/src/main.jsx b/client/src/main.jsx index b9a1a6d..a8fad7e 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -1,10 +1,15 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { ChakraProvider, createSystem, defaultConfig } from "@chakra-ui/react" import './index.css' import App from './App.jsx' +const system = createSystem(defaultConfig) + createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/client/src/tsconfig.app.json b/client/src/tsconfig.app.json new file mode 100644 index 0000000..c205775 --- /dev/null +++ b/client/src/tsconfig.app.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "paths": { + "@/*": ["./src/*"] + } + } +} \ No newline at end of file diff --git a/server/server/index.js b/server/server/index.js index a562b89..e470c92 100644 --- a/server/server/index.js +++ b/server/server/index.js @@ -4,14 +4,13 @@ const cors = require('cors'); const admin = require('firebase-admin'); const mysql = require('mysql'); const dotenv = require('dotenv'); -const assert = require('assert'); -// const db = require('./config'); +const util = require('util'); // Load environment variables from .env file dotenv.config(); // Firebase Admin SDK configuration -const serviceAccount = require('./config/serviceAccountKey.json'); // Update with your serviceAccountKey.json file +const serviceAccount = require('./config/serviceAccountKey.json'); // Update path if needed admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); @@ -22,16 +21,16 @@ const db = mysql.createConnection({ user: process.env.MYSQL_USER, password: process.env.MYSQL_PASSWORD, database: process.env.MYSQL_DATABASE, - port: process.env.MYSQL_PORT, }); - + +// Alternative local config (commented) // const db = mysql.createConnection({ -// host: "localhost", -// user: "root", -// password: "", -// database:"iskalnik_dztps" +// host: "localhost", +// user: "root", +// password: "", +// database:"iskalnik_dztps" // }); - + db.connect((err) => { if (err) { console.error('MySQL connection failed:', err); @@ -40,19 +39,11 @@ db.connect((err) => { } }); -// Helper function to promisify database queries -const query = (sql, params) => { - return new Promise((resolve, reject) => { - db.query(sql, params, (err, result) => { - if (err) { - reject(err); - } else { - resolve(result); - } - }); - }); -}; - +// --- Promise helpers for mysql (node-mysql, not mysql2) --- +const query = util.promisify(db.query).bind(db); +const beginTransaction = util.promisify(db.beginTransaction).bind(db); +const commit = util.promisify(db.commit).bind(db); +const rollback = util.promisify(db.rollback).bind(db); // Express application const app = express(); @@ -81,8 +72,6 @@ const authenticateFirebaseToken = async (req, res, next) => { // Protected route example app.get('/express', authenticateFirebaseToken, (req, res) => { - // Access user ID from req.uid - // Fetch data from MySQL using the user ID or perform other operations res.json({ message: 'Protected route accessed successfully' }); }); @@ -97,114 +86,115 @@ app.listen(PORT, () => { * clan *********************************/ -function formClanStruct(from, id=null) { +function formClanStruct(from, id = null) { if (!id) id = from.id; return { - 'id': id, - 'id_izkaznica': from.id_izkaznica || "", - 'ime': from.ime || "", - 'priimek': from.priimek || "", - 'spol': from.spol || "", - 'datum_rojstva': from.datum_rojstva || "", - 'kraj_rojstva': from.kraj_rojstva || "", - 'drzavljanstvo': from.drzavljanstvo || "", - 'solska_izobrazba': from.solska_izobrazba || "", - 'telefondoma': from.telefondoma || "", - 'telefonsluzba': from.telefonsluzba || "", - 'telefonmobi': from.telefonmobi || "", - 'email': from.email || "", - 'spletnastran': from.spletnastran || "", - 'naslovbivalisca': from.naslovbivalisca || "", - 'ulica': from.ulica || "", - 'postna_stevilka': from.postna_stevilka || "", - 'posta': from.posta || "", - 'osnovni_poklic': from.osnovni_poklic || "", - 'sedanja_zaposlitev': from.sedanja_zaposlitev || "", - 'prevajalska_praksa': from.prevajalska_praksa || "", - 'nacin_pridobivanja_znanja_tujih_jezikov': from.nacin_pridobivanja_znanja_tujih_jezikov || "", - 'datum_vclanitve': from.datum_vclanitve || "", - 'materni_jezik': from.materni_jezik || "", - 'objava_v_iskalniku': from.objava_v_iskalniku || 0 + id, + id_izkaznica: from.id_izkaznica || "", + ime: from.ime || "", + priimek: from.priimek || "", + spol: from.spol || "", + datum_rojstva: from.datum_rojstva || "", + kraj_rojstva: from.kraj_rojstva || "", + drzavljanstvo: from.drzavljanstvo || "", + solska_izobrazba: from.solska_izobrazba || "", + telefondoma: from.telefondoma || "", + telefonsluzba: from.telefonsluzba || "", + telefonmobi: from.telefonmobi || "", + email: from.email || "", + spletnastran: from.spletnastran || "", + naslovbivalisca: from.naslovbivalisca || "", + ulica: from.ulica || "", + postna_stevilka: from.postna_stevilka || "", + posta: from.posta || "", + osnovni_poklic: from.osnovni_poklic || "", + sedanja_zaposlitev: from.sedanja_zaposlitev || "", + prevajalska_praksa: from.prevajalska_praksa || "", + nacin_pridobivanja_znanja_tujih_jezikov: from.nacin_pridobivanja_znanja_tujih_jezikov || "", + datum_vclanitve: from.datum_vclanitve || "", + materni_jezik: from.materni_jezik || "", + objava_v_iskalniku: from.objava_v_iskalniku || 0 } } -function formClanValueArray(res) { - const body = res.body; - return [body.id_izkaznica, body.ime, body.priimek, body.spol, body.datum_rojstva ? new Date(body.datum_rojstva) : null, +function formClanValueArray(req) { + const body = req.body || {}; + return [ + body.id_izkaznica, body.ime, body.priimek, body.spol, + body.datum_rojstva ? new Date(body.datum_rojstva) : null, body.kraj_rojstva, body.drzavljanstvo, body.solska_izobrazba, body.telefondoma, body.telefonsluzba, body.telefonmobi, body.email, body.spletnastran, body.naslovbivalisca, - body.ulica, body.postna_stevilka, body.posta, body.osnovni_poklic, body.sedanja_zaposlitev, body.prevajalska_praksa, body.nacin_pridobivanja_znanja_tujih_jezikov, body.datum_vclanitve ? new Date(body.datum_vclanitve) : null, + body.ulica, body.postna_stevilka, body.posta, body.osnovni_poklic, body.sedanja_zaposlitev, + body.prevajalska_praksa, body.nacin_pridobivanja_znanja_tujih_jezikov, + body.datum_vclanitve ? new Date(body.datum_vclanitve) : null, body.materni_jezik, body.objava_v_iskalniku ]; } app.get("/express/api/v1/clan/", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT * - FROM clan`, (err, result, fields) => { - if (err) { - res.sendStatus(500); - return; - } + db.query(`SELECT * FROM clan`, (err, result) => { + if (err) return res.sendStatus(500); const out = result.map(row => formClanStruct(row)); res.status(200).json(out); }); }); app.get("/express/api/v1/clan/:id", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT * - FROM clan - WHERE id = ?`, [req.params.id], (err, result, fields) => { - if (result.length === 0 || err) { - res.sendStatus(404); - return; - } - const first = result[0]; - const out = formClanStruct(first); - res.status(200).json(out); + db.query(`SELECT * FROM clan WHERE id = ?`, [req.params.id], (err, result) => { + if (err) return res.sendStatus(500); + if (!result.length) return res.sendStatus(404); + res.status(200).json(formClanStruct(result[0])); }); }); app.post("/express/api/v1/clan/", authenticateFirebaseToken, (req, res) => { - db.query(`INSERT INTO clan ( - id_izkaznica, ime, priimek, spol, datum_rojstva, kraj_rojstva, - drzavljanstvo, solska_izobrazba, telefondoma, telefonsluzba, - telefonmobi, email, spletnastran, naslovbivalisca, ulica, - postna_stevilka, posta, osnovni_poklic, sedanja_zaposlitev, prevajalska_praksa, nacin_pridobivanja_znanja_tujih_jezikov, datum_vclanitve, - materni_jezik, objava_v_iskalniku - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, formClanValueArray(req), (err, result) => { - if (!err && result.affectedRows === 1) { - res.status(200).json(formClanStruct(req.body, result.insertId)); - } else { - res.status(200).json(err); - // res.sendStatus(400); + db.query( + `INSERT INTO clan ( + id_izkaznica, ime, priimek, spol, datum_rojstva, kraj_rojstva, + drzavljanstvo, solska_izobrazba, telefondoma, telefonsluzba, + telefonmobi, email, spletnastran, naslovbivalisca, ulica, + postna_stevilka, posta, osnovni_poklic, sedanja_zaposlitev, prevajalska_praksa, + nacin_pridobivanja_znanja_tujih_jezikov, datum_vclanitve, materni_jezik, objava_v_iskalniku + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + formClanValueArray(req), + (err, result) => { + if (!err && result.affectedRows === 1) { + res.status(200).json(formClanStruct(req.body, result.insertId)); + } else { + console.error("INSERT /clan error:", err); + res.sendStatus(400); + } } - }); + ); }); app.put("/express/api/v1/clan/:id", authenticateFirebaseToken, (req, res) => { const valueArr = formClanValueArray(req); valueArr.push(req.params.id); - db.query(`UPDATE clan - SET id_izkaznica=?, ime=?, priimek=?, spol=?, datum_rojstva=?, kraj_rojstva=?, - drzavljanstvo=?, solska_izobrazba=?, telefondoma=?, telefonsluzba=?, - telefonmobi=?, email=?, spletnastran=?, naslovbivalisca=?, ulica=?, - postna_stevilka=?, posta=?, osnovni_poklic=?, sedanja_zaposlitev=?, prevajalska_praksa=?, nacin_pridobivanja_znanja_tujih_jezikov=?, datum_vclanitve=?, - materni_jezik=?, objava_v_iskalniku=? - WHERE id=? - `, valueArr, (err, result, fields) => { - if (!err && result.affectedRows === 1) { - res.status(200).json(formClanStruct(req.body, req.params.id)); - } else { - res.sendStatus(400); + db.query( + `UPDATE clan + SET id_izkaznica=?, ime=?, priimek=?, spol=?, datum_rojstva=?, kraj_rojstva=?, + drzavljanstvo=?, solska_izobrazba=?, telefondoma=?, telefonsluzba=?, + telefonmobi=?, email=?, spletnastran=?, naslovbivalisca=?, ulica=?, + postna_stevilka=?, posta=?, osnovni_poklic=?, sedanja_zaposlitev=?, prevajalska_praksa=?, + nacin_pridobivanja_znanja_tujih_jezikov=?, datum_vclanitve=?, + materni_jezik=?, objava_v_iskalniku=? + WHERE id=?`, + valueArr, + (err, result) => { + if (!err && result.affectedRows === 1) { + res.status(200).json(formClanStruct(req.body, req.params.id)); + } else { + console.error("UPDATE /clan error:", err); + res.sendStatus(400); + } } - }); + ); }); app.delete("/express/api/v1/clan/:id", authenticateFirebaseToken, (req, res) => { - db.query(`DELETE FROM clan - WHERE id = ?`, [req.params.id], (err, result, fields) => { + db.query(`DELETE FROM clan WHERE id = ?`, [req.params.id], (err, result) => { if (!err && result.affectedRows === 1) { res.sendStatus(200); } else { @@ -213,294 +203,418 @@ app.delete("/express/api/v1/clan/:id", authenticateFirebaseToken, (req, res) => }); }); + /********************************** - * prevod + * prevod (translations) *********************************/ +// Safe helpers that reject on error function getVloge(prevodID) { - return new Promise((resolve) => { - db.query(`SELECT v.id as 'id', vloga, izpis_zenska, izpis_moski - FROM prevod_vloga as pv - JOIN vloga as v - ON pv.vloga_id = v.id - WHERE pv.prevod_id = ? - `, [prevodID], (err, result, fields) => { - if (err) throw err; - function formVloga(row) { - return { - 'id': row.id, - 'vloga': row.vloga, - 'izpis_zenska': row.izpis_zenska, - 'izpis_moski': row.izpis_moski - } + return new Promise((resolve, reject) => { + db.query( + `SELECT v.id AS id, v.vloga, v.izpis_zenska, v.izpis_moski + FROM prevod_vloga pv + JOIN vloga v ON pv.vloga_id = v.id + WHERE pv.prevod_id = ?`, + [prevodID], + (err, result) => { + if (err) return reject(err); + resolve(result.map(row => ({ + id: row.id, + vloga: row.vloga, + izpis_zenska: row.izpis_zenska, + izpis_moski: row.izpis_moski + }))); } - resolve(result.map(row => formVloga(row))); - }); + ); }); - - } + function getPodrocja(prevodID) { - return new Promise((resolve) => { - db.query(`SELECT p.id as 'id', podrocje - FROM prevod_podrocje as pp - JOIN podrocje as p - ON pp.podrocje_id = p.id - WHERE pp.prevod_id = ? - `, [prevodID], (err, result, fields) => { - if (err) throw err; - function formPodrocje(row) { - return { - 'id': row.id, - 'podrocje': row.podrocje, - } + return new Promise((resolve, reject) => { + db.query( + `SELECT p.id AS id, p.podrocje + FROM prevod_podrocje pp + JOIN podrocje p ON pp.podrocje_id = p.id + WHERE pp.prevod_id = ?`, + [prevodID], + (err, result) => { + if (err) return reject(err); + resolve(result.map(row => ({ id: row.id, podrocje: row.podrocje }))); } - - resolve(result.map(row => formPodrocje(row))); - }); + ); }); - } -async function formPrevodStruct(from, id=null) { - if (!id) id = from.id; +async function formPrevodStruct(from, id = null) { + const pid = id || from.id; const out = { - 'id': id, - 'id_clan': from.id_clan, - 'rang': from.rang, - 'iz': from.iz, - 'v': from.v, - 'licenca_DZTPS': from.licenca_DZTPS, + id: pid, + id_clan: from.id_clan, + rang: from.rang, + iz: from.iz, + v: from.v, + licenca_DZTPS: from.licenca_DZTPS }; - const podrocja = await getPodrocja(id); - const vloge = await getVloge(id); - out.podrocja = podrocja; - out.vloge = vloge; + out.podrocja = await getPodrocja(pid); + out.vloge = await getVloge(pid); return out; } + app.get("/express/api/v1/clan/:id/prevod/", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT p.id as 'id', id_clan, rang, iz.jezik as 'iz', v.jezik as 'v', licenca_DZTPS - FROM prevod as p - JOIN jezik as iz - ON p.iz_id = iz.id - JOIN jezik as v - ON p.v_id = v.id - WHERE id_clan = ? - `, [req.params.id], async (err, result, fields) => { - if (err) { - res.sendStatus(500); - return; + db.query( + `SELECT p.id AS id, p.id_clan, p.rang, iz.jezik AS iz, v.jezik AS v, p.licenca_DZTPS + FROM prevod p + JOIN jezik iz ON p.iz_id = iz.id + JOIN jezik v ON p.v_id = v.id + WHERE p.id_clan = ?`, + [req.params.id], + async (err, result) => { + if (err) { + console.error("DB error (prevod list):", err); + return res.sendStatus(500); + } + try { + const out = await Promise.all(result.map(row => formPrevodStruct(row))); + res.status(200).json(out); + } catch (e) { + console.error("Error assembling prevod response:", e); + res.sendStatus(500); + } } - const out = await Promise.all(result.map(row => formPrevodStruct(row))); - res.status(200).json(out); - }); + ); }); app.get("/express/api/v1/clan/:id/prevod/:pid", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT p.id as 'id', id_clan, rang, iz.jezik as 'iz', v.jezik as 'v', licenca_DZTPS - FROM prevod as p - JOIN jezik as iz - ON p.iz_id = iz.id - JOIN jezik as v - ON p.v_id = v.id - WHERE p.id_clan = ? AND p.id = ? - `, [req.params.id, req.params.pid], async (err, result, fields) => { - if (err || result.length === 0) { - res.sendStatus(404); - return; + db.query( + `SELECT p.id AS id, p.id_clan, p.rang, iz.jezik AS iz, v.jezik AS v, p.licenca_DZTPS + FROM prevod p + JOIN jezik iz ON p.iz_id = iz.id + JOIN jezik v ON p.v_id = v.id + WHERE p.id_clan = ? AND p.id = ?`, + [req.params.id, req.params.pid], + async (err, result) => { + if (err) return res.sendStatus(500); + if (!result.length) return res.sendStatus(404); + try { + const out = await formPrevodStruct(result[0]); + res.status(200).json(out); + } catch (e) { + console.error("Error assembling single prevod:", e); + res.sendStatus(500); + } } - const first = result[0]; - const out = await formPrevodStruct(first); - res.status(200).json(out); - }); + ); }); +// Helpers to add/remove relations async function addPrevodVloga(prevodID, vloga) { - await query(`INSERT INTO prevod_vloga -(prevod_id, vloga_id) -VALUES -(?, ( - SELECT v.id - FROM vloga as v - WHERE v.vloga = ? -)) -`, [prevodID, vloga]); + await query( + `INSERT INTO prevod_vloga (prevod_id, vloga_id) + VALUES (?, (SELECT v.id FROM vloga v WHERE v.vloga = ?))`, + [prevodID, vloga] + ); } async function removePrevodVloga(prevodID, vloga) { - await query(`DELETE FROM prevod_vloga -WHERE prevod_id=? AND vloga_id=( - SELECT v.id - FROM vloga as v - WHERE v.vloga = ? - ) -`, [prevodID, vloga]); + await query( + `DELETE FROM prevod_vloga + WHERE prevod_id = ? AND vloga_id = (SELECT v.id FROM vloga v WHERE v.vloga = ?)`, + [prevodID, vloga] + ); } - async function addPrevodPodrocje(prevodID, podrocje) { - await query(`INSERT INTO prevod_podrocje -(prevod_id, podrocje_id) -VALUES -(?, ( - SELECT p.id - FROM podrocje as p - WHERE p.podrocje=? - )) -`, [prevodID, podrocje]); + await query( + `INSERT INTO prevod_podrocje (prevod_id, podrocje_id) + VALUES (?, (SELECT p.id FROM podrocje p WHERE p.podrocje = ?))`, + [prevodID, podrocje] + ); } - async function removePrevodPodrocje(prevodID, podrocje) { - await query(`DELETE FROM prevod_podrocje -WHERE prevod_id=? AND podrocje_id=( - SELECT p.id - FROM podrocje as p - WHERE p.podrocje = ? - ) -`, [prevodID, podrocje]); + await query( + `DELETE FROM prevod_podrocje + WHERE prevod_id = ? AND podrocje_id = (SELECT p.id FROM podrocje p WHERE p.podrocje = ?)`, + [prevodID, podrocje] + ); } -app.post("/express/api/v1/clan/:id/prevod/", async (req, res) => { - await db.beginTransaction(); +function namesFromArray(arr, key) { + if (!Array.isArray(arr)) return []; + return arr.map(x => (typeof x === 'string' ? x : (x?.[key] ?? ''))).filter(Boolean); +} + +// Create translation +app.post("/express/api/v1/clan/:id/prevod/", authenticateFirebaseToken, async (req, res) => { + const { id: pathId } = req.params; + const { id_clan, rang, iz, v, licenca_DZTPS, podrocja = [], vloge = [] } = req.body || {}; + + if (String(pathId) !== String(id_clan)) { + return res.status(400).json({ message: "id_clan in body must match :id in path" }); + } + if (!rang || !iz || !v) { + return res.status(400).json({ message: "Missing required fields: rang, iz, v" }); + } + try { - assert(req.body.id_clan == req.params.id); - const prevod = await query(`INSERT INTO prevod -(id_clan, rang, iz_id, v_id, licenca_DZTPS) -VALUES -(?, ?, ( - SELECT iz.id - FROM jezik as iz - WHERE iz.jezik=? -), ( - SELECT v.id - FROM jezik as v - WHERE v.jezik=? - ), ?) -`, [req.body.id_clan, req.body.rang, req.body.iz, req.body.v, req.body.licenca_DZTPS]); + await beginTransaction(); - const prevodID = prevod.insertId; - for (let podrocje of req.body.podrocja) { - await addPrevodPodrocje(prevodID, podrocje.podrocje); - } - for (let vloga of req.body.vloge) { - await addPrevodVloga(prevodID, vloga.vloga); + // Resolve language IDs + const langs = await query(`SELECT id, jezik FROM jezik WHERE jezik IN (?, ?)`, [iz, v]); + const izRow = langs.find(r => r.jezik === iz); + const vRow = langs.find(r => r.jezik === v); + if (!izRow || !vRow) { + await rollback(); + return res.status(400).json({ message: "Unknown language in iz or v" }); } - await db.commit(); - res.status(200).json(await formPrevodStruct(req.body, prevodID)); + const insert = await query( + `INSERT INTO prevod (id_clan, rang, iz_id, v_id, licenca_DZTPS) + VALUES (?, ?, ?, ?, ?)`, + [id_clan, rang, izRow.id, vRow.id, licenca_DZTPS ?? 0] + ); + const prevodID = insert.insertId; + + // Podrocja + const podNames = namesFromArray(podrocja, 'podrocje'); + if (podNames.length) { + const rows = await query( + `SELECT id, podrocje FROM podrocje WHERE podrocje IN (${podNames.map(() => '?').join(',')})`, + podNames + ); + const mapIds = new Map(rows.map(r => [r.podrocje, r.id])); + const missing = podNames.filter(n => !mapIds.has(n)); + if (missing.length) { + await rollback(); + return res.status(400).json({ message: "Unknown podrocje", details: missing }); + } + const values = podNames.map(n => [prevodID, mapIds.get(n)]); + await query( + `INSERT INTO prevod_podrocje (prevod_id, podrocje_id) + VALUES ${values.map(() => '(?, ?)').join(',')}`, + values.flat() + ); + } + + // Vloge + const vlogaNames = namesFromArray(vloge, 'vloga'); + if (vlogaNames.length) { + const rows = await query( + `SELECT id, vloga FROM vloga WHERE vloga IN (${vlogaNames.map(() => '?').join(',')})`, + vlogaNames + ); + const mapIds = new Map(rows.map(r => [r.vloga, r.id])); + const missing = vlogaNames.filter(n => !mapIds.has(n)); + if (missing.length) { + await rollback(); + return res.status(400).json({ message: "Unknown vloga", details: missing }); + } + const values = vlogaNames.map(n => [prevodID, mapIds.get(n)]); + await query( + `INSERT INTO prevod_vloga (prevod_id, vloga_id) + VALUES ${values.map(() => '(?, ?)').join(',')}`, + values.flat() + ); + } + + await commit(); + + const [row] = await query( + `SELECT p.id AS id, p.id_clan, p.rang, iz.jezik AS iz, v.jezik AS v, p.licenca_DZTPS + FROM prevod p + JOIN jezik iz ON p.iz_id = iz.id + JOIN jezik v ON p.v_id = v.id + WHERE p.id = ?`, + [prevodID] + ); + const payload = await formPrevodStruct(row); + return res.status(200).json(payload); + } catch (e) { - db.rollback(); - res.sendStatus(400); + try { await rollback(); } catch {} + console.error("POST /clan/:id/prevod error:", e); + return res.status(400).json({ message: "Failed to create prevod", error: e && e.message }); } }); -app.put("/express/api/v1/clan/:id/prevod/:pid", async (req, res) => { - await db.beginTransaction(); +// Update translation +app.put("/express/api/v1/clan/:id/prevod/:pid", authenticateFirebaseToken, async (req, res) => { + const { id: pathId, pid } = req.params; + const { id_clan, rang, iz, v, licenca_DZTPS, podrocja = [], vloge = [] } = req.body || {}; + + if (String(pathId) !== String(id_clan)) { + return res.status(400).json({ message: "id_clan in body must match :id in path" }); + } + try { - await query(`UPDATE prevod - SET id_clan=?, - rang=?, - iz_id=(SELECT iz.id - FROM jezik as iz - WHERE iz.jezik = ?), - v_id=(SELECT v.id - FROM jezik as v - WHERE v.jezik = ?), - licenca_DZTPS=? - WHERE id_clan=? AND id=? - `, [req.body.id_clan, req.body.rang, req.body.iz, req.body.v, req.body.licenca_DZTPS, req.params.id, req.params.pid]); + await beginTransaction(); - const prevod = await formPrevodStruct(req.body); + // Resolve language IDs if provided strings + const langsNeeded = []; + if (iz) langsNeeded.push(iz); + if (v) langsNeeded.push(v); - // Synchronize podrocja - const oldPodrocja = prevod.podrocja; - const newPodrocja = req.body.podrocja; - for (let podrocje of oldPodrocja) { - // Podrocje not present in new -> delete - if (!newPodrocja.some(p => p.id === podrocje.id)) - await removePrevodPodrocje(prevod.id, podrocje.podrocje); - } - for (let podrocje of newPodrocja) { - // Podrocje present in new but not in old -> add - if (!oldPodrocja.some(p => p.id === podrocje.id)) - await addPrevodPodrocje(prevod.id, podrocje.podrocje); - } - // Synchronize vloge - const oldVloge = prevod.vloge; - const newVloge = req.body.vloge; - for (let vloga of oldVloge) { - // Vloga not present in new -> delete - if (!newVloge.some(v => v.id === vloga.id)) - await removePrevodVloga(prevod.id, vloga.vloga); - } - for (let vloga of newVloge) { - // Vloga present in new but not in old -> add - if (!oldVloge.some(v => v.id === vloga.id)) - await addPrevodVloga(prevod.id, vloga.vloga); + let izId = null, vId = null; + if (langsNeeded.length) { + const langRows = await query( + `SELECT id, jezik FROM jezik WHERE jezik IN (${langsNeeded.map(() => '?').join(',')})`, + langsNeeded + ); + const map = new Map(langRows.map(r => [r.jezik, r.id])); + if (iz && !map.has(iz)) { await rollback(); return res.status(400).json({ message: "Unknown language (iz)" }); } + if (v && !map.has(v)) { await rollback(); return res.status(400).json({ message: "Unknown language (v)" }); } + if (iz) izId = map.get(iz); + if (v) vId = map.get(v); } - await db.commit(); - res.status(200).json(await formPrevodStruct(req.body)); + // Build dynamic UPDATE (keep current values if not provided) + const sets = []; + const params = []; + if (id_clan != null) { sets.push(`id_clan = ?`); params.push(id_clan); } + if (rang != null) { sets.push(`rang = ?`); params.push(rang); } + if (izId != null) { sets.push(`iz_id = ?`); params.push(izId); } + if (vId != null) { sets.push(`v_id = ?`); params.push(vId); } + if (licenca_DZTPS != null) { sets.push(`licenca_DZTPS = ?`); params.push(licenca_DZTPS); } + + if (sets.length) { + params.push(pathId, pid); + const sql = `UPDATE prevod SET ${sets.join(', ')} WHERE id_clan = ? AND id = ?`; + const upd = await query(sql, params); + if (upd.affectedRows === 0) { + await rollback(); + return res.sendStatus(404); + } + } + + // Fetch old relations BEFORE syncing + const oldPodrocja = await getPodrocja(pid); + const oldVloge = await getVloge(pid); + + // Sync Podrocja + const newPodrocjaNames = namesFromArray(podrocja, 'podrocje'); + if (newPodrocjaNames.length) { + // Map names -> ids + const rows = await query( + `SELECT id, podrocje FROM podrocje WHERE podrocje IN (${newPodrocjaNames.map(() => '?').join(',')})`, + newPodrocjaNames + ); + const mapIds = new Map(rows.map(r => [r.podrocje, r.id])); + const missing = newPodrocjaNames.filter(n => !mapIds.has(n)); + if (missing.length) { + await rollback(); + return res.status(400).json({ message: "Unknown podrocje", details: missing }); + } + + // Compute deltas by name + const oldNames = new Set(oldPodrocja.map(p => p.podrocje)); + const newNames = new Set(newPodrocjaNames); + + // Deletes + for (const name of oldNames) { + if (!newNames.has(name)) { + await removePrevodPodrocje(pid, name); + } + } + // Adds + for (const name of newNames) { + if (!oldNames.has(name)) { + await addPrevodPodrocje(pid, name); + } + } + } + + // Sync Vloge + const newVlogaNames = namesFromArray(vloge, 'vloga'); + if (newVlogaNames.length) { + const rows = await query( + `SELECT id, vloga FROM vloga WHERE vloga IN (${newVlogaNames.map(() => '?').join(',')})`, + newVlogaNames + ); + const mapIds = new Map(rows.map(r => [r.vloga, r.id])); + const missing = newVlogaNames.filter(n => !mapIds.has(n)); + if (missing.length) { + await rollback(); + return res.status(400).json({ message: "Unknown vloga", details: missing }); + } + + const oldNames = new Set(oldVloge.map(v => v.vloga)); + const newNames = new Set(newVlogaNames); + + for (const name of oldNames) { + if (!newNames.has(name)) { + await removePrevodVloga(pid, name); + } + } + for (const name of newNames) { + if (!oldNames.has(name)) { + await addPrevodVloga(pid, name); + } + } + } + + await commit(); + + // Return the updated record + const [row] = await query( + `SELECT p.id AS id, p.id_clan, p.rang, iz.jezik AS iz, v.jezik AS v, p.licenca_DZTPS + FROM prevod p + JOIN jezik iz ON p.iz_id = iz.id + JOIN jezik v ON p.v_id = v.id + WHERE p.id = ?`, + [pid] + ); + if (!row) return res.sendStatus(404); + const payload = await formPrevodStruct(row); + res.status(200).json(payload); + } catch (e) { - db.rollback(); - res.sendStatus(400); + try { await rollback(); } catch {} + console.error("PUT /clan/:id/prevod/:pid error:", e); + res.status(400).json({ message: "Failed to update prevod", error: e && e.message }); } }); -app.delete("/express/api/v1/clan/:id/prevod/:pid", async (req, res) => { - await db.beginTransaction(); +app.delete("/express/api/v1/clan/:id/prevod/:pid", authenticateFirebaseToken, async (req, res) => { try { - await query(`DELETE FROM prevod_podrocje WHERE prevod_id=?`, [req.params.pid]); - await query(`DELETE FROM prevod_vloga WHERE prevod_id=?`, [req.params.pid]); - await query(`DELETE FROM prevod WHERE id_clan=? AND id=?`, [req.params.id, req.params.pid]); - await db.commit(); + await beginTransaction(); + await query(`DELETE FROM prevod_podrocje WHERE prevod_id = ?`, [req.params.pid]); + await query(`DELETE FROM prevod_vloga WHERE prevod_id = ?`, [req.params.pid]); + const del = await query(`DELETE FROM prevod WHERE id_clan = ? AND id = ?`, [req.params.id, req.params.pid]); + if (!del.affectedRows) { await rollback(); return res.sendStatus(404); } + await commit(); res.sendStatus(200); } catch (e) { - db.rollback(); + try { await rollback(); } catch {} + console.error("DELETE /clan/:id/prevod/:pid error:", e); res.sendStatus(400); } }); + /********************************** * jezik *********************************/ -function formJezikStruct(from, id=null) { +function formJezikStruct(from, id = null) { if (!id) id = from.id; - return { - 'id': id, - 'jezik': from.jezik - }; + return { id, jezik: from.jezik }; } app.get("/express/api/v1/jezik/", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT id, jezik - FROM jezik`, (err, result, fields) => { - if (err) { - res.sendStatus(500); - return; - } - const out = result.map(row => formJezikStruct(row)); - res.status(200).json(out); + db.query(`SELECT id, jezik FROM jezik`, (err, result) => { + if (err) return res.sendStatus(500); + res.status(200).json(result.map(row => formJezikStruct(row))); }); }); app.get("/express/api/v1/jezik/:id", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT id, jezik - FROM jezik - WHERE id = ?`, [req.params.id], (err, result, fields) => { - if (result.length === 0 || err) { - res.sendStatus(404); - return; - } - const first = result[0]; - const out = formJezikStruct(first); - res.status(200).json(out); + db.query(`SELECT id, jezik FROM jezik WHERE id = ?`, [req.params.id], (err, result) => { + if (err) return res.sendStatus(500); + if (!result.length) return res.sendStatus(404); + res.status(200).json(formJezikStruct(result[0])); }); }); app.post("/express/api/v1/jezik/", authenticateFirebaseToken, (req, res) => { - db.query(`INSERT INTO jezik - (jezik) - VALUES (?)`, [req.body.jezik], (err, result) => { + db.query(`INSERT INTO jezik (jezik) VALUES (?)`, [req.body.jezik], (err, result) => { if (!err && result.affectedRows === 1) { res.status(200).json(formJezikStruct(req.body, result.insertId)); } else { @@ -510,9 +624,7 @@ app.post("/express/api/v1/jezik/", authenticateFirebaseToken, (req, res) => { }); app.put("/express/api/v1/jezik/:id", authenticateFirebaseToken, (req, res) => { - db.query(`UPDATE jezik - SET jezik = ? - WHERE id = ?`, [req.body.jezik, req.params.id], (err, result) => { + db.query(`UPDATE jezik SET jezik = ? WHERE id = ?`, [req.body.jezik, req.params.id], (err, result) => { if (!err && result.affectedRows === 1) { res.status(200).json(formJezikStruct(req.body, req.params.id)); } else { @@ -522,8 +634,7 @@ app.put("/express/api/v1/jezik/:id", authenticateFirebaseToken, (req, res) => { }); app.delete("/express/api/v1/jezik/:id", authenticateFirebaseToken, (req, res) => { - db.query(`DELETE FROM jezik - WHERE id = ?`, [req.params.id], (err, result, fields) => { + db.query(`DELETE FROM jezik WHERE id = ?`, [req.params.id], (err, result) => { if (!err && result.affectedRows === 1) { res.sendStatus(200); } else { @@ -532,73 +643,66 @@ app.delete("/express/api/v1/jezik/:id", authenticateFirebaseToken, (req, res) => }); }); + /********************************** * vloga *********************************/ -function formVlogaStruct(from, id=null) { +function formVlogaStruct(from, id = null) { if (!id) id = from.id; return { - 'id': id, - 'vloga': from.vloga, - 'izpis_zenska': from.izpis_zenska, - 'izpis_moski': from.izpis_moski + id, + vloga: from.vloga, + izpis_zenska: from.izpis_zenska, + izpis_moski: from.izpis_moski }; } app.get("/express/api/v1/vloga/", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT id, vloga, izpis_zenska, izpis_moski - FROM vloga`, (err, result, fields) => { - if (err) { - res.sendStatus(500); - return; - } - const out = result.map(row => formVlogaStruct(row)); - res.status(200).json(out); + db.query(`SELECT id, vloga, izpis_zenska, izpis_moski FROM vloga`, (err, result) => { + if (err) return res.sendStatus(500); + res.status(200).json(result.map(row => formVlogaStruct(row))); }); }); app.get("/express/api/v1/vloga/:id", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT id, vloga, izpis_zenska, izpis_moski - FROM vloga - WHERE id = ?`, [req.params.id], (err, result, fields) => { - if (result.length === 0 || err) { - res.sendStatus(404); - return; - } - const first = result[0]; - const out = formVlogaStruct(first); - res.status(200).json(out); + db.query(`SELECT id, vloga, izpis_zenska, izpis_moski FROM vloga WHERE id = ?`, [req.params.id], (err, result) => { + if (err) return res.sendStatus(500); + if (!result.length) return res.sendStatus(404); + res.status(200).json(formVlogaStruct(result[0])); }); }); app.post("/express/api/v1/vloga/", authenticateFirebaseToken, (req, res) => { - db.query(`INSERT INTO vloga - (vloga, izpis_zenska, izpis_moski) - VALUES (?, ?, ?)`, [req.body.vloga, req.body.izpis_zenska, req.body.izpis_moski], (err, result) => { - if (!err && result.affectedRows === 1) { - res.status(200).json(formVlogaStruct(req.body, result.insertId)); - } else { - res.sendStatus(400); + db.query( + `INSERT INTO vloga (vloga, izpis_zenska, izpis_moski) VALUES (?, ?, ?)`, + [req.body.vloga, req.body.izpis_zenska, req.body.izpis_moski], + (err, result) => { + if (!err && result.affectedRows === 1) { + res.status(200).json(formVlogaStruct(req.body, result.insertId)); + } else { + res.sendStatus(400); + } } - }); + ); }); app.put("/express/api/v1/vloga/:id", authenticateFirebaseToken, (req, res) => { - db.query(`UPDATE vloga - SET vloga = ?, izpis_zenska = ?, izpis_moski = ? - WHERE id = ?`, [req.body.vloga, req.body.izpis_zenska, req.body.izpis_moski, req.params.id], (err, result) => { - if (!err && result.affectedRows === 1) { - res.status(200).json(formVlogaStruct(req.body, req.params.id)); - } else { - res.sendStatus(400); + db.query( + `UPDATE vloga SET vloga = ?, izpis_zenska = ?, izpis_moski = ? WHERE id = ?`, + [req.body.vloga, req.body.izpis_zenska, req.body.izpis_moski, req.params.id], + (err, result) => { + if (!err && result.affectedRows === 1) { + res.status(200).json(formVlogaStruct(req.body, req.params.id)); + } else { + res.sendStatus(400); + } } - }); + ); }); app.delete("/express/api/v1/vloga/:id", authenticateFirebaseToken, (req, res) => { - db.query(`DELETE FROM vloga - WHERE id = ?`, [req.params.id], (err, result, fields) => { + db.query(`DELETE FROM vloga WHERE id = ?`, [req.params.id], (err, result) => { if (!err && result.affectedRows === 1) { res.sendStatus(200); } else { @@ -607,48 +711,33 @@ app.delete("/express/api/v1/vloga/:id", authenticateFirebaseToken, (req, res) => }); }); + /********************************** * podrocje *********************************/ -function formPodrocjeStruct(from, id=null) { +function formPodrocjeStruct(from, id = null) { if (!id) id = from.id; - return { - 'id': id, - 'podrocje': from.podrocje, - }; + return { id, podrocje: from.podrocje }; } app.get("/express/api/v1/podrocje", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT id, podrocje - FROM podrocje`, (err, result, fields) => { - if (err) { - res.sendStatus(500); - return; - } - const out = result.map(row => formPodrocjeStruct(row)); - res.status(200).json(out); + db.query(`SELECT id, podrocje FROM podrocje`, (err, result) => { + if (err) return res.sendStatus(500); + res.status(200).json(result.map(row => formPodrocjeStruct(row))); }); }); app.get("/express/api/v1/podrocje/:id", authenticateFirebaseToken, (req, res) => { - db.query(`SELECT id, podrocje - FROM podrocje - WHERE id = ?`, [req.params.id], (err, result, fields) => { - if (result.length === 0 || err) { - res.sendStatus(404); - return; - } - const first = result[0]; - const out = formPodrocjeStruct(first); - res.status(200).json(out); + db.query(`SELECT id, podrocje FROM podrocje WHERE id = ?`, [req.params.id], (err, result) => { + if (err) return res.sendStatus(500); + if (!result.length) return res.sendStatus(404); + res.status(200).json(formPodrocjeStruct(result[0])); }); }); app.post("/express/api/v1/podrocje/", authenticateFirebaseToken, (req, res) => { - db.query(`INSERT INTO podrocje - (podrocje) - VALUES (?)`, [req.body.podrocje], (err, result) => { + db.query(`INSERT INTO podrocje (podrocje) VALUES (?)`, [req.body.podrocje], (err, result) => { if (!err && result.affectedRows === 1) { res.status(200).json(formPodrocjeStruct(req.body, result.insertId)); } else { @@ -658,37 +747,39 @@ app.post("/express/api/v1/podrocje/", authenticateFirebaseToken, (req, res) => { }); app.put("/express/api/v1/podrocje/:id", authenticateFirebaseToken, (req, res) => { - db.query(`UPDATE podrocje - SET podrocje = ? - WHERE id = ?`, [req.body.podrocje, req.params.id], (err, result, fields) => { - if (!err && result.affectedRows === 1) { - res.status(200).json(formPodrocjeStruct(req.body, req.params.id)); - } else { - res.sendStatus(400); + db.query( + `UPDATE podrocje SET podrocje = ? WHERE id = ?`, + [req.body.podrocje, req.params.id], + (err, result) => { + if (!err && result.affectedRows === 1) { + res.status(200).json(formPodrocjeStruct(req.body, req.params.id)); + } else { + res.sendStatus(400); + } } - }); + ); }); app.delete("/express/api/v1/podrocje/:id", authenticateFirebaseToken, (req, res) => { - db.query(`DELETE FROM podrocje - WHERE id = ?`, [req.params.id], (err, result, fields) => { + db.query(`DELETE FROM podrocje WHERE id = ?`, [req.params.id], (err, result) => { if (!err && result.affectedRows === 1) { res.sendStatus(200); } else { res.sendStatus(400); } }); - }); -// ISKALNIK +/********************************** + * ISKALNIK (search) + *********************************/ app.get('/express_backend', (req, res) => { res.send({ express: `YOUR BACKEND IS CONNECTED` }); }); -app.post('/express_backend/api/v1/iskanje', (req,res) => { +app.post('/express_backend/api/v1/iskanje', (req, res) => { const iskanjeIme = req.body.iskanjeNiz; const iskanjeCrka = req.body.iskanjeCrkaPriimek; const iskanjeIz = req.body.iskanjeIzvirniJezik; @@ -701,6 +792,7 @@ app.post('/express_backend/api/v1/iskanje', (req,res) => { // Build query const params = []; const queryConditions = []; + if (iskanjeIme) { queryConditions.push("(LOWER(CONCAT(c.ime, ' ', c.priimek)) LIKE LOWER(?))"); params.push(`%${iskanjeIme}%`); @@ -718,219 +810,217 @@ app.post('/express_backend/api/v1/iskanje', (req,res) => { params.push(iskanjeV); } const generateQuestionMarks = (n) => Array.from({ length: n }, () => '?').join(','); - if (iskanjeRangi) { + if (Array.isArray(iskanjeRangi) && iskanjeRangi.length) { queryConditions.push(`(p.rang IN (${generateQuestionMarks(iskanjeRangi.length)}))`); params.push(...iskanjeRangi); } - - if (iskanjeLicenca) { - queryConditions.push("(p.licenca_DZTPS = ?)") - params.push(iskanjeLicenca) + if (iskanjeLicenca != null) { + queryConditions.push("(p.licenca_DZTPS = ?)"); + params.push(iskanjeLicenca); } + const where = [ + "c.objava_v_iskalniku = 1", + ...(queryConditions.length ? queryConditions : []) + ].join(" AND "); - db.query(`SELECT c.id as 'id' , c.ime as 'ime', c.priimek as 'priimek', c.spol as 'spol', c.telefondoma as 'telefondoma', c.telefonsluzba as 'telefonsluzba', - c.telefonmobi as 'telefonmobi', c.email as 'email', c.spletnastran as 'spletnastran', - p.rang as 'rang', iz.jezik as iz, v.jezik as v, p.licenca_DZTPS as 'licenca', - ( - SELECT GROUP_CONCAT(po.podrocje SEPARATOR ';') - FROM prevod_podrocje as pp - JOIN podrocje as po - ON pp.podrocje_id = po.id - WHERE pp.prevod_id = p.id - GROUP BY pp.prevod_id - ) as 'podrocja', - ( - SELECT GROUP_CONCAT(vl.vloga SEPARATOR ';') - FROM prevod_vloga as pv - JOIN vloga as vl - ON pv.vloga_id = vl.id - WHERE pv.prevod_id = p.id - GROUP BY pv.prevod_id - ) as 'vloge' -FROM clan as c -JOIN prevod as p -ON c.id = p.id_clan -JOIN jezik as iz -ON p.iz_id = iz.id -JOIN jezik as v -ON p.v_id = v.id -WHERE c.objava_v_iskalniku = 1 -WHERE ${queryConditions.length ? queryConditions.join("AND") : "1=1"}`, - params, (err, rows, fields)=> { - if (err) throw err; - if (rows.length == 0) { - res.send({}); - return; + db.query( + `SELECT + c.id AS id, c.ime AS ime, c.priimek AS priimek, c.spol AS spol, + c.telefondoma AS telefondoma, c.telefonsluzba AS telefonsluzba, + c.telefonmobi AS telefonmobi, c.email AS email, c.spletnastran AS spletnastran, + p.rang AS rang, iz.jezik AS iz, v.jezik AS v, p.licenca_DZTPS AS licenca, + ( + SELECT GROUP_CONCAT(po.podrocje SEPARATOR ';') + FROM prevod_podrocje pp + JOIN podrocje po ON pp.podrocje_id = po.id + WHERE pp.prevod_id = p.id + GROUP BY pp.prevod_id + ) AS podrocja, + ( + SELECT GROUP_CONCAT(vl.vloga SEPARATOR ';') + FROM prevod_vloga pv + JOIN vloga vl ON pv.vloga_id = vl.id + WHERE pv.prevod_id = p.id + GROUP BY pv.prevod_id + ) AS vloge + FROM clan c + JOIN prevod p ON c.id = p.id_clan + JOIN jezik iz ON p.iz_id = iz.id + JOIN jezik v ON p.v_id = v.id + WHERE ${where}`, + params, + (err, rows) => { + if (err) { + console.error("Search error:", err); + return res.sendStatus(500); } + if (!rows.length) return res.send({}); let out = []; for (let row of rows) { if (out.length <= 0 || out[out.length - 1].id !== row.id) { out.push({ - "id": row.id, - "ime": row.ime, - "priimek": row.priimek, - "spol": row.spol, - "telefondoma": row.telefondoma, - "telefonmobi": row.telefonmobi, - "telefonsluzba": row.telefonsluzba, - "email": row.email, - "spletnastran": row.spletnastran, - "prevodi": [], + id: row.id, + ime: row.ime, + priimek: row.priimek, + spol: row.spol, + telefondoma: row.telefondoma, + telefonmobi: row.telefonmobi, + telefonsluzba: row.telefonsluzba, + email: row.email, + spletnastran: row.spletnastran, + prevodi: [], }); } let podrocja = row.podrocja?.split(';') || []; let vloge = row.vloge?.split(';') || []; + if (iskanjePodrocja.size) { - podrocja = podrocja.filter((el) => iskanjePodrocja.has(el)); - if (podrocja.length === 0) continue; + podrocja = podrocja.filter(el => iskanjePodrocja.has(el)); + if (!podrocja.length) continue; } if (iskanjeVloge.size) { - vloge = vloge.filter((el) => iskanjeVloge.has(el)); - if (vloge.length === 0) continue; + vloge = vloge.filter(el => iskanjeVloge.has(el)); + if (!vloge.length) continue; } - out[out.length - 1].prevodi.push({ - "rang": row.rang, - "iz": row.iz, - "v": row.v, - "strokovnopodrocje": row.podrocja?.split(';'), - "vloga": row.vloge?.split(';'), + rang: row.rang, + iz: row.iz, + v: row.v, + strokovnopodrocje: row.podrocja?.split(';') || [], + vloga: row.vloge?.split(';') || [], }); } - out = out.filter((row) => row.prevodi.length > 0); + out = out.filter(row => row.prevodi.length > 0); res.send(JSON.stringify(out, null, 4)); - }); - + } + ); }); app.get("/express_backend/api/get/user/:user_id", (req, res) => { - db.query(`SELECT c.id as 'id' , c.ime as 'ime', c.priimek as 'priimek', c.spol as 'spol', c.telefondoma as 'telefondoma', c.telefonsluzba as 'telefonsluzba', - c.telefonmobi as 'telefonmobi', c.email as 'email', c.spletnastran as 'spletnastran', - p.rang as 'rang', iz.jezik as iz, v.jezik as v, - ( - SELECT GROUP_CONCAT(po.podrocje SEPARATOR ';') - FROM prevod_podrocje as pp - JOIN podrocje as po - ON pp.podrocje_id = po.id - WHERE pp.prevod_id = p.id - GROUP BY pp.prevod_id - ) as 'podrocja', - ( - SELECT GROUP_CONCAT(vl.vloga SEPARATOR ';') - FROM prevod_vloga as pv - JOIN vloga as vl - ON pv.vloga_id = vl.id - WHERE pv.prevod_id = p.id - GROUP BY pv.prevod_id - ) as 'vloge' -FROM clan as c -JOIN prevod as p -ON c.id = p.id_clan -JOIN jezik as iz -ON p.iz_id = iz.id -JOIN jezik as v -ON p.v_id = v.id -WHERE c.id = ? -WHERE c.objava_v_iskalniku = 1 -ORDER BY rang;`, [req.params.user_id], (err, rows, fields)=> { - if (err) throw err; - if (rows.length === 0) { - res.send({}); - return; + db.query( + `SELECT + c.id AS id, c.ime AS ime, c.priimek AS priimek, c.spol AS spol, + c.telefondoma AS telefondoma, c.telefonsluzba AS telefonsluzba, + c.telefonmobi AS telefonmobi, c.email AS email, c.spletnastran AS spletnastran, + p.id AS id_prevod, p.rang AS rang, iz.jezik AS iz, v.jezik AS v, + ( + SELECT GROUP_CONCAT(po.podrocje SEPARATOR ';') + FROM prevod_podrocje pp + JOIN podrocje po ON pp.podrocje_id = po.id + WHERE pp.prevod_id = p.id + GROUP BY pp.prevod_id + ) AS podrocja, + ( + SELECT GROUP_CONCAT(vl.vloga SEPARATOR ';') + FROM prevod_vloga pv + JOIN vloga vl ON pv.vloga_id = vl.id + WHERE pv.prevod_id = p.id + GROUP BY pv.prevod_id + ) AS vloge + FROM clan c + JOIN prevod p ON c.id = p.id_clan + JOIN jezik iz ON p.iz_id = iz.id + JOIN jezik v ON p.v_id = v.id + WHERE c.id = ? AND c.objava_v_iskalniku = 1 + ORDER BY p.rang`, + [req.params.user_id], + (err, rows) => { + if (err) { + console.error("get/user error:", err); + return res.sendStatus(500); + } + if (!rows.length) return res.send({}); + + const first = rows[0]; + const out = { + id: first.id, + ime: first.ime, + priimek: first.priimek, + telefondoma: first.telefondoma, + telefonmobi: first.telefonmobi, + telefonsluzba: first.telefonsluzba, + email: first.email, + spletnastran: first.spletnastran, + prevodi: [], + }; + for (let row of rows) { + out.prevodi.push({ + id: row.id_prevod, + rang: row.rang, + iz: row.iz, + v: row.v, + strokovnopodrocje: row.podrocja?.split(';') || [], + vloga: row.vloge?.split(';') || [], + }); + } + res.send(JSON.stringify(out, null, 4)); } - - const first = rows[0]; - - let out = { - "id": first.id, - "ime": first.ime, - "priimek": first.priimek, - "telefondoma": first.telefondoma, - "telefonmobi": first.telefonmobi, - "telefonsluzba": first.telefonsluzba, - "email": first.email, - "spletnastran": first.spletnastran, - "prevodi": [], - }; - for (let row of rows) { - out.prevodi.push({ - "id": row.id_prevod, - "rang": row.rang, - "iz": row.iz, - "v": row.v, - "strokovnopodrocje": row.podrocja?.split(';'), - "vloga": row.vloge?.split(';'), - }); - } - res.send(JSON.stringify(out, null, 4)); - }); - + ); }); app.get("/express_backend/api/getAll", (req, res) => { - db.query(`SELECT c.id as 'id' , c.ime as 'ime', c.priimek as 'priimek', c.spol as 'spol', c.telefondoma as 'telefondoma', c.telefonsluzba as 'telefonsluzba', - c.telefonmobi as 'telefonmobi', c.email as 'email', c.spletnastran as 'spletnastran', - p.rang as 'rang', iz.jezik as iz, v.jezik as v, - ( - SELECT GROUP_CONCAT(po.podrocje SEPARATOR ';') - FROM prevod_podrocje as pp - JOIN podrocje as po - ON pp.podrocje_id = po.id - WHERE pp.prevod_id = p.id - GROUP BY pp.prevod_id - ) as 'podrocja', - ( - SELECT GROUP_CONCAT(vl.vloga SEPARATOR ';') - FROM prevod_vloga as pv - JOIN vloga as vl - ON pv.vloga_id = vl.id - WHERE pv.prevod_id = p.id - GROUP BY pv.prevod_id - ) as 'vloge' -FROM clan as c -JOIN prevod as p -ON c.id = p.id_clan -JOIN jezik as iz -ON p.iz_id = iz.id -JOIN jezik as v -ON p.v_id = v.id -WHERE c.objava_v_iskalniku = 1 -;`, (err, rows, fields)=> { - if (err) throw err; + db.query( + `SELECT + c.id AS id, c.ime AS ime, c.priimek AS priimek, c.spol AS spol, + c.telefondoma AS telefondoma, c.telefonsluzba AS telefonsluzba, + c.telefonmobi AS telefonmobi, c.email AS email, c.spletnastran AS spletnastran, + p.rang AS rang, iz.jezik AS iz, v.jezik AS v, + ( + SELECT GROUP_CONCAT(po.podrocje SEPARATOR ';') + FROM prevod_podrocje pp + JOIN podrocje po ON pp.podrocje_id = po.id + WHERE pp.prevod_id = p.id + GROUP BY pp.prevod_id + ) AS podrocja, + ( + SELECT GROUP_CONCAT(vl.vloga SEPARATOR ';') + FROM prevod_vloga pv + JOIN vloga vl ON pv.vloga_id = vl.id + WHERE pv.prevod_id = p.id + GROUP BY pv.prevod_id + ) AS vloge + FROM clan c + JOIN prevod p ON c.id = p.id_clan + JOIN jezik iz ON p.iz_id = iz.id + JOIN jezik v ON p.v_id = v.id + WHERE c.objava_v_iskalniku = 1`, + (err, rows) => { + if (err) { + console.error("getAll error:", err); + return res.sendStatus(500); + } - // Group prevodi per user - let out = []; - for (let row of rows) { - if (out.length <= 0 || out[out.length - 1].id !== row.id) { - out.push({ - "id": row.id, - "ime": row.ime, - "priimek": row.priimek, - "spol": row.spol, - "telefondoma": row.telefondoma, - "telefonmobi": row.telefonmobi, - "telefonsluzba": row.telefonsluzba, - "email": row.email, - "spletnastran": row.spletnastran, - "prevodi": [], + // Group prevodi per user + let out = []; + for (let row of rows) { + if (out.length <= 0 || out[out.length - 1].id !== row.id) { + out.push({ + id: row.id, + ime: row.ime, + priimek: row.priimek, + spol: row.spol, + telefondoma: row.telefondoma, + telefonmobi: row.telefonmobi, + telefonsluzba: row.telefonsluzba, + email: row.email, + spletnastran: row.spletnastran, + prevodi: [], + }); + } + out[out.length - 1].prevodi.push({ + rang: row.rang, + iz: row.iz, + v: row.v, + strokovnopodrocje: row.podrocja?.split(';') || [], + vloga: row.vloge?.split(';') || [], }); } - out[out.length - 1].prevodi.push({ - "rang": row.rang, - "iz": row.iz, - "v": row.v, - "strokovnopodrocje": row.podrocja?.split(';'), - "vloga": row.vloge?.split(';'), - }); + res.send(JSON.stringify(out, null, 4)); } - res.send(JSON.stringify(out, null, 4)); - }); -}) + ); +}); - -// app.listen(PORT, () => { -// console.log(`Server listening on port ${PORT}`); -// }); \ No newline at end of file +// app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });