Skip to content

Commit d3bed68

Browse files
authored
Merge pull request #27 from bbwheroes/feature/fachrichtung_und_projektmethode
Fachrichtung und Projektmethode
2 parents 8028ef4 + c6cf338 commit d3bed68

File tree

16 files changed

+432
-80
lines changed

16 files changed

+432
-80
lines changed

backend/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ WORKDIR /app/backend
2626

2727
EXPOSE 3000
2828

29-
CMD ["bun", "run", "src/index.js"]
29+
CMD ["sh", "-c", "bun run src/db/migrate.js && bun run src/index.js"]

backend/src/db/migrate.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,24 @@ async function migrate() {
1717
first_name VARCHAR(100),
1818
last_name VARCHAR(100),
1919
topic TEXT,
20-
submission_date DATE
20+
submission_date DATE,
21+
specialty VARCHAR(50),
22+
project_method VARCHAR(20)
2123
)
2224
`;
2325

26+
await sql`
27+
DO $$
28+
BEGIN
29+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'user_profiles' AND column_name = 'specialty') THEN
30+
ALTER TABLE user_profiles ADD COLUMN specialty VARCHAR(50);
31+
END IF;
32+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'user_profiles' AND column_name = 'project_method') THEN
33+
ALTER TABLE user_profiles ADD COLUMN project_method VARCHAR(20);
34+
END IF;
35+
END $$;
36+
`;
37+
2438
await sql`
2539
CREATE TABLE IF NOT EXISTS ticked_requirements (
2640
id SERIAL PRIMARY KEY,

backend/src/middleware/auth.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ export const optionalAuth = async (req, res, next) => {
2828
try {
2929
const decoded = jwt.verify(token, JWT_SECRET);
3030
req.userId = decoded.userId;
31-
} catch (error) {
32-
console.error('Invalid token:', error);
31+
} catch {
3332
req.userId = null;
3433
}
3534
}

backend/src/routes/users.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ router.get('/profile', authMiddleware, async (req, res) => {
1313
up.last_name,
1414
up.topic,
1515
up.submission_date,
16+
up.specialty,
17+
up.project_method,
1618
up.user_id
1719
FROM user_profiles up
1820
WHERE up.user_id = ${req.userId}
@@ -30,7 +32,7 @@ router.get('/profile', authMiddleware, async (req, res) => {
3032
});
3133

3234
router.put('/profile', authMiddleware, async (req, res) => {
33-
const { firstName, lastName, topic, submissionDate } = req.body;
35+
const { firstName, lastName, topic, submissionDate, specialty, projectMethod } = req.body;
3436

3537
try {
3638
const [profile] = await sql`
@@ -39,7 +41,9 @@ router.put('/profile', authMiddleware, async (req, res) => {
3941
first_name = COALESCE(${firstName}, first_name),
4042
last_name = COALESCE(${lastName}, last_name),
4143
topic = COALESCE(${topic}, topic),
42-
submission_date = COALESCE(${submissionDate}, submission_date)
44+
submission_date = COALESCE(${submissionDate}, submission_date),
45+
specialty = COALESCE(${specialty}, specialty),
46+
project_method = COALESCE(${projectMethod}, project_method)
4347
WHERE user_id = ${req.userId}
4448
RETURNING *
4549
`;

backend/src/test/setup.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export async function setupTestDatabase() {
1616
first_name VARCHAR(100),
1717
last_name VARCHAR(100),
1818
topic TEXT,
19-
submission_date DATE
19+
submission_date DATE,
20+
specialty VARCHAR(50),
21+
project_method VARCHAR(20)
2022
)
2123
`;
2224

criterias.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,10 @@
325325
"requirements": [
326326
"Es wurde eine Beschreibung der Testinfrastruktur und des Umfelds bereitgestellt, sodass eine externe Fachperson die Tests mit gleichen Ergebnissen reproduzieren kann.",
327327
"Verbesserungspotential wie auch Nacharbeiten wurden identifiziert. Falls weder Verbesserungspotential besteht noch Nacharbeit nötig ist, ist dies nachvollziehbar begründet.",
328-
"Linear: Relevante Testszenarien wie auch Testkomponenten (bspw. Funktionen, Daten, Dokumente, Performance, Schnittstellen etc.) sind inkl. der erwarteten Ergebnisse beschrieben.",
329-
"Linear: Die Tests wurden basierend auf den Testszenarien durchgeführt. Die Ergebnisse sind korrekt und übersichtlich dokumentiert.",
330-
"Agil: Es existiert eine Definition of Done (DoD), die festlegt, welche Bedingungen erfüllt sein müssen, damit eine User Story oder ein Arbeitspaket als abgeschlossen gilt.",
331-
"Agil: Die Tests wurden basierend auf der DoD durchgeführt. Die Ergebnisse sind korrekt und übersichtlich dokumentiert."
328+
{ "text": "Relevante Testszenarien wie auch Testkomponenten (bspw. Funktionen, Daten, Dokumente, Performance, Schnittstellen etc.) sind inkl. der erwarteten Ergebnisse beschrieben.", "projectMethod": "Linear" },
329+
{ "text": "Die Tests wurden basierend auf den Testszenarien durchgeführt. Die Ergebnisse sind korrekt und übersichtlich dokumentiert.", "projectMethod": "Linear" },
330+
{ "text": "Es existiert eine Definition of Done (DoD), die festlegt, welche Bedingungen erfüllt sein müssen, damit eine User Story oder ein Arbeitspaket als abgeschlossen gilt.", "projectMethod": "Agil" },
331+
{ "text": "Die Tests wurden basierend auf der DoD durchgeführt. Die Ergebnisse sind korrekt und übersichtlich dokumentiert.", "projectMethod": "Agil" }
332332
],
333333
"stages": {
334334
"3": {

frontend/src/App.css

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ body {
4343
.main-content {
4444
flex: 1;
4545
padding: 2rem;
46-
max-width: 1400px;
46+
max-width: 1200px;
4747
margin: 0 auto;
4848
width: 100%;
4949
}
@@ -59,7 +59,7 @@ body {
5959
}
6060

6161
.header-content {
62-
max-width: 1200px;
62+
max-width: 1000px;
6363
margin: 0 auto;
6464
padding: 1rem 2rem;
6565
display: flex;
@@ -335,7 +335,8 @@ button {
335335
}
336336

337337
.form-group input,
338-
.form-group textarea {
338+
.form-group textarea,
339+
.form-group select {
339340
background: var(--surface-light);
340341
border: 1px solid var(--border);
341342
color: var(--text);
@@ -345,8 +346,29 @@ button {
345346
font-family: inherit;
346347
}
347348

349+
.form-group select {
350+
cursor: pointer;
351+
appearance: none;
352+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23cbd5e1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
353+
background-repeat: no-repeat;
354+
background-position: right 0.75rem center;
355+
background-size: 1.25rem;
356+
padding-right: 2.5rem;
357+
}
358+
359+
.form-group select:hover {
360+
border-color: var(--primary);
361+
}
362+
363+
.form-group select option {
364+
background: var(--surface);
365+
color: var(--text);
366+
padding: 0.5rem;
367+
}
368+
348369
.form-group input:focus,
349-
.form-group textarea:focus {
370+
.form-group textarea:focus,
371+
.form-group select:focus {
350372
outline: 2px solid var(--primary);
351373
outline-offset: 0;
352374
}
@@ -357,6 +379,29 @@ button {
357379
gap: 1rem;
358380
}
359381

382+
.form-divider {
383+
display: flex;
384+
align-items: center;
385+
gap: 1rem;
386+
color: var(--text-secondary);
387+
font-size: 0.875rem;
388+
}
389+
390+
.form-divider::before,
391+
.form-divider::after {
392+
content: '';
393+
flex: 1;
394+
height: 1px;
395+
background: var(--border);
396+
}
397+
398+
.btn-secondary {
399+
display: flex;
400+
align-items: center;
401+
justify-content: center;
402+
gap: 0.5rem;
403+
}
404+
360405
.login-footer {
361406
margin-top: 1.5rem;
362407
text-align: center;

frontend/src/components/CategorySection.jsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,29 @@ import { useState } from 'react';
22
import { ChevronDown, ChevronUp } from 'lucide-react';
33
import CriteriaItem from './CriteriaItem';
44

5-
export default function CategorySection({ category, criterias, evaluations, onUpdate }) {
5+
export default function CategorySection({ category, criterias, evaluations, onUpdate, projectMethod }) {
66
const [isOpen, setIsOpen] = useState(true);
77

8+
const getFilteredRequirements = requirements => {
9+
return requirements.filter(req => {
10+
if (typeof req === 'string') return true;
11+
return !req.projectMethod || req.projectMethod === projectMethod;
12+
});
13+
};
14+
15+
const getRequirementText = req => (typeof req === 'string' ? req : req.text);
16+
817
const calculateCategoryProgress = () => {
918
let totalCompleted = 0;
1019
let totalItems = 0;
1120

1221
criterias.forEach(criteria => {
22+
const filteredReqs = getFilteredRequirements(criteria.requirements);
23+
const validRequirementTexts = filteredReqs.map(getRequirementText);
1324
const ticked = evaluations[criteria.id]?.tickedRequirements || [];
14-
totalCompleted += ticked.length;
15-
totalItems += criteria.requirements.length;
25+
const validTicked = ticked.filter(t => validRequirementTexts.includes(t));
26+
totalCompleted += validTicked.length;
27+
totalItems += filteredReqs.length;
1628
});
1729

1830
return totalItems > 0 ? Math.round((totalCompleted / totalItems) * 100) : 0;
@@ -45,6 +57,7 @@ export default function CategorySection({ category, criterias, evaluations, onUp
4557
tickedRequirements={evaluations[criteria.id]?.tickedRequirements || []}
4658
note={evaluations[criteria.id]?.note || ''}
4759
onUpdate={data => onUpdate(criteria.id, data)}
60+
projectMethod={projectMethod}
4861
/>
4962
))}
5063
</div>

frontend/src/components/CriteriaItem.jsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@ import { useState } from 'react';
22
import { ChevronDown, ChevronUp } from 'lucide-react';
33
import { calculateGrade } from '../../../shared/gradeCalculation.js';
44

5-
export default function CriteriaItem({ criteria, tickedRequirements, note, onUpdate }) {
5+
export default function CriteriaItem({ criteria, tickedRequirements, note, onUpdate, projectMethod }) {
66
const [isOpen, setIsOpen] = useState(false);
77
const [localNote, setLocalNote] = useState(note || '');
88

9+
const filteredRequirements = criteria.requirements.filter(req => {
10+
if (typeof req === 'string') return true;
11+
return !req.projectMethod || req.projectMethod === projectMethod;
12+
});
13+
14+
const getRequirementText = req => (typeof req === 'string' ? req : req.text);
15+
16+
const validRequirementTexts = filteredRequirements.map(getRequirementText);
17+
const validTickedRequirements = tickedRequirements.filter(t => validRequirementTexts.includes(t));
18+
919
const handleRequirementToggle = requirement => {
1020
const newTicked = tickedRequirements.includes(requirement)
1121
? tickedRequirements.filter(r => r !== requirement)
@@ -25,7 +35,7 @@ export default function CriteriaItem({ criteria, tickedRequirements, note, onUpd
2535
onUpdate({ tickedRequirements, note: localNote });
2636
};
2737

28-
const points = calculateGrade(criteria, tickedRequirements);
38+
const points = calculateGrade(criteria, validTickedRequirements, projectMethod);
2939
const gradeClass = points !== null ? `grade-${points}` : 'grade-none';
3040

3141
return (
@@ -45,7 +55,7 @@ export default function CriteriaItem({ criteria, tickedRequirements, note, onUpd
4555
</span>
4656
)}
4757
<span className="criteria-progress">
48-
{tickedRequirements.length}/{criteria.requirements.length}
58+
{validTickedRequirements.length}/{filteredRequirements.length}
4959
</span>
5060
{isOpen ? <ChevronUp /> : <ChevronDown />}
5161
</div>
@@ -54,19 +64,22 @@ export default function CriteriaItem({ criteria, tickedRequirements, note, onUpd
5464
{isOpen && (
5565
<div className="criteria-content">
5666
<div className="requirements-list">
57-
{criteria.requirements.map((requirement, index) => (
58-
<div key={index} className="requirement-item">
59-
<label>
60-
<input
61-
type={criteria.selection === 'single' ? 'radio' : 'checkbox'}
62-
name={`criteria-${criteria.id}`}
63-
checked={tickedRequirements.includes(requirement)}
64-
onChange={() => handleRequirementToggle(requirement)}
65-
/>
66-
<span className="requirement-text">{requirement}</span>
67-
</label>
68-
</div>
69-
))}
67+
{filteredRequirements.map((requirement, index) => {
68+
const text = getRequirementText(requirement);
69+
return (
70+
<div key={index} className="requirement-item">
71+
<label>
72+
<input
73+
type={criteria.selection === 'single' ? 'radio' : 'checkbox'}
74+
name={`criteria-${criteria.id}`}
75+
checked={tickedRequirements.includes(text)}
76+
onChange={() => handleRequirementToggle(text)}
77+
/>
78+
<span className="requirement-text">{text}</span>
79+
</label>
80+
</div>
81+
);
82+
})}
7083
</div>
7184

7285
<div className="note-section">

frontend/src/components/Header.jsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useNavigate } from 'react-router-dom';
22
import { useAuth } from '../hooks/useAuth';
3+
import { storage } from '../services/storage';
34
import { LogIn, LogOut, UserCircle, Home, Github } from 'lucide-react';
45
import { APP_VERSION } from '../utils/version';
56

67
export default function Header() {
78
const { user, isAuthenticated, logout } = useAuth();
89
const navigate = useNavigate();
10+
const localProfile = storage.getProfile();
911

1012
const handleLogout = () => {
1113
logout();
@@ -51,6 +53,22 @@ export default function Header() {
5153
Abmelden
5254
</button>
5355
</>
56+
) : localProfile.firstName ? (
57+
<>
58+
<div className="user-info">
59+
<UserCircle size={18} />
60+
<span>
61+
{localProfile.firstName} {localProfile.lastName || ''}
62+
</span>
63+
</div>
64+
<button onClick={() => navigate('/onboarding?edit=true')} className="btn-secondary-small">
65+
Profil
66+
</button>
67+
<button onClick={() => navigate('/login')} className="btn-secondary-small">
68+
<LogIn size={16} />
69+
Anmelden
70+
</button>
71+
</>
5472
) : (
5573
<button onClick={() => navigate('/login')} className="btn-primary-small">
5674
<LogIn size={16} />

0 commit comments

Comments
 (0)