github-actions[bot] commited on
Commit
b55899d
·
1 Parent(s): cc06e81

🚀 Auto-deploy from GitHub Actions - 2026-02-28 23:08:07

Browse files
.env.example ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================
2
+ # NeonDB Configuration
3
+ # ==============================================
4
+ # Get your connection string from: https://console.neon.tech
5
+ # Format: postgresql://user:password@host/database?sslmode=require
6
+ NEON_DB="postgresql://user:password@ep-xyz-123.us-east-2.aws.neon.tech/neondb?sslmode=require"
7
+
8
+ # ==============================================
9
+ # HuggingFace Configuration (Optional - for local testing)
10
+ # ==============================================
11
+ # Only needed if you want to test HF deployment locally
12
+ # Get your token from: https://huggingface.co/settings/tokens
13
+ HF_TOKEN="hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
14
+ HF_USERNAME="your-username"
15
+ SPACE_NAME="job-tracker"
16
+
17
+ # ==============================================
18
+ # Application Configuration (Optional)
19
+ # ==============================================
20
+ # Uncomment and modify if needed
21
+
22
+ # Application name
23
+ # APP_NAME="Job Tracker"
24
+
25
+ # Debug mode (development only)
26
+ # DEBUG=False
27
+
28
+ # Session timeout (minutes)
29
+ # SESSION_TIMEOUT=30
30
+
31
+ # Max file upload size (MB)
32
+ # MAX_UPLOAD_SIZE=10
33
+
34
+ # ==============================================
35
+ # Notes
36
+ # ==============================================
37
+ # 1. Copy this file to .env and fill in your values
38
+ # 2. NEVER commit .env to git (it's in .gitignore)
39
+ # 3. For production, set NEON_DB in HuggingFace Space secrets
40
+ # 4. For GitHub Actions, set HF_TOKEN, HF_USERNAME, SPACE_NAME in GitHub Secrets
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ .venv/
10
+
11
+ # Secrets
12
+ .env
13
+ *.pem
14
+ *.key
15
+
16
+ # IDEs
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+
22
+ # OS
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # Streamlit
27
+ .streamlit/secrets.toml
28
+
29
+ # Tests
30
+ .coverage
31
+ htmlcov/
32
+ .pytest_cache/
.streamlit/config.toml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#4CAF50"
3
+ backgroundColor = "#FFFFFF"
4
+ secondaryBackgroundColor = "#F0F2F6"
5
+ textColor = "#262730"
6
+ font = "sans serif"
7
+
8
+ [server]
9
+ headless = true
10
+ port = 7860
11
+ enableCORS = false
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Dépendances système si besoin (ex: pour psycopg2)
6
+ RUN apt-get update && apt-get install -y \
7
+ gcc \
8
+ postgresql-client \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copie des requirements
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copie du code
16
+ COPY . .
17
+
18
+ # Port Streamlit par défaut
19
+ EXPOSE 7860
20
+
21
+ # Healthcheck (optionnel mais propre)
22
+ HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health || exit 1
23
+
24
+ # Commande de lancement
25
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Albert ROMANO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,12 @@
1
  ---
2
- title: Job Tracker
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: indigo
6
- sdk: docker
7
  pinned: false
8
- license: mit
9
- short_description: 'job-tracker for aapplication '
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: Job Application Tracker
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker # ← IMPORTANT si tu utilises Dockerfile
7
  pinned: false
 
 
8
  ---
9
 
10
+ # Job Application Tracker
11
+
12
+ Track your job applications with ease.
app.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from utils.auth import login, logout, register, is_logged_in
3
+ from database.db_connection import init_database
4
+
5
+ # Configuration de la page
6
+ st.set_page_config(
7
+ page_title="Job Tracker",
8
+ page_icon="📊",
9
+ layout="wide",
10
+ initial_sidebar_state="expanded"
11
+ )
12
+
13
+ # Initialisation de la base de données
14
+ try:
15
+ init_database()
16
+ except Exception as e:
17
+ st.error(f"Erreur d'initialisation de la base de données : {e}")
18
+
19
+ # Initialisation du session_state
20
+ if 'logged_in' not in st.session_state:
21
+ st.session_state['logged_in'] = False
22
+ if 'user_id' not in st.session_state:
23
+ st.session_state['user_id'] = None
24
+ if 'username' not in st.session_state:
25
+ st.session_state['username'] = None
26
+
27
+ # ============ PAGE DE LOGIN/REGISTER ============
28
+
29
+ if not is_logged_in():
30
+ st.title("🔐 Job Application Tracker")
31
+ st.markdown("### Bienvenue ! Connectez-vous ou créez un compte")
32
+
33
+ tab1, tab2 = st.tabs(["Se connecter", "Créer un compte"])
34
+
35
+ # Tab Login
36
+ with tab1:
37
+ st.subheader("Connexion")
38
+ login_username = st.text_input("Nom d'utilisateur", key="login_user")
39
+ login_password = st.text_input("Mot de passe", type="password", key="login_pass")
40
+
41
+ if st.button("Se connecter", type="primary"):
42
+ if login_username and login_password:
43
+ if login(login_username, login_password):
44
+ st.success(f"Bienvenue {login_username} !")
45
+ st.rerun()
46
+ else:
47
+ st.error("Identifiants incorrects")
48
+ else:
49
+ st.warning("Veuillez remplir tous les champs")
50
+
51
+ # Tab Register
52
+ with tab2:
53
+ st.subheader("Créer un compte")
54
+ register_username = st.text_input("Nom d'utilisateur", key="register_user")
55
+ register_password = st.text_input("Mot de passe", type="password", key="register_pass")
56
+ register_password_confirm = st.text_input("Confirmer le mot de passe", type="password", key="register_pass_confirm")
57
+
58
+ if st.button("Créer mon compte", type="primary"):
59
+ if register_username and register_password and register_password_confirm:
60
+ if register_password != register_password_confirm:
61
+ st.error("Les mots de passe ne correspondent pas")
62
+ elif len(register_password) < 6:
63
+ st.error("Le mot de passe doit contenir au moins 6 caractères")
64
+ else:
65
+ success, message = register(register_username, register_password)
66
+ if success:
67
+ st.success(message)
68
+ st.info("Vous pouvez maintenant vous connecter !")
69
+ else:
70
+ st.error(message)
71
+ else:
72
+ st.warning("Veuillez remplir tous les champs")
73
+
74
+ # ============ PAGE PRINCIPALE (si connecté) ============
75
+
76
+ else:
77
+ # Sidebar avec infos user et logout
78
+ with st.sidebar:
79
+ st.markdown(f"### 👤 {st.session_state['username']}")
80
+ st.markdown("---")
81
+
82
+ if st.button("🚪 Se déconnecter", type="secondary"):
83
+ logout()
84
+ st.rerun()
85
+
86
+ st.markdown("---")
87
+ st.markdown("### Navigation")
88
+ st.info("Utilisez les pages dans le menu ci-dessus pour :\n- 📝 Ajouter une candidature\n- 📊 Voir vos candidatures")
89
+
90
+ # Page d'accueil
91
+ st.title("📊 Job Application Tracker")
92
+ st.markdown("### Bienvenue sur votre tableau de bord")
93
+
94
+ st.success("✅ Vous êtes connecté(e)")
95
+
96
+ st.markdown("""
97
+ ### Comment utiliser l'application ?
98
+
99
+ 1. **📝 Nouvelle candidature** : Ajoutez vos candidatures avec toutes les informations importantes
100
+ 2. **📊 Mes candidatures** : Consultez, modifiez et suivez l'évolution de vos candidatures
101
+
102
+ ---
103
+
104
+ #### Fonctionnalités disponibles :
105
+ - ✅ Suivi de vos candidatures
106
+ - ✅ Gestion des statuts (En attente, Relancé, Refusé, Accepté)
107
+ - ✅ Tri et filtrage
108
+ - ✅ Modification et suppression
109
+
110
+ """)
111
+
112
+ # Statistiques rapides (optionnel)
113
+ from database.queries import get_user_applications
114
+
115
+ try:
116
+ applications = get_user_applications(st.session_state['user_id'])
117
+
118
+ col1, col2, col3, col4 = st.columns(4)
119
+
120
+ with col1:
121
+ st.metric("Total candidatures", len(applications))
122
+
123
+ with col2:
124
+ en_attente = len([app for app in applications if app['status'] == 'En attente'])
125
+ st.metric("En attente", en_attente)
126
+
127
+ with col3:
128
+ acceptees = len([app for app in applications if app['status'] == 'Accepté'])
129
+ st.metric("Acceptées", acceptees)
130
+
131
+ with col4:
132
+ refusees = len([app for app in applications if app['status'] == 'Refusé'])
133
+ st.metric("Refusées", refusees)
134
+
135
+ except Exception as e:
136
+ st.error(f"Erreur lors du chargement des statistiques : {e}")
database/db_connection.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from sqlalchemy import create_engine, text
3
+ from sqlalchemy.orm import sessionmaker
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ # Récupération de la connection string
9
+ DATABASE_URL = os.getenv("NEON_DB")
10
+
11
+ # Création de l'engine SQLAlchemy
12
+ engine = create_engine(DATABASE_URL, echo=False)
13
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
14
+
15
+ def get_db_session():
16
+ """Retourne une session SQLAlchemy"""
17
+ return SessionLocal()
18
+
19
+ def init_database():
20
+ """Initialise les tables si elles n'existent pas"""
21
+ with engine.connect() as conn:
22
+ # Table users
23
+ conn.execute(text("""
24
+ CREATE TABLE IF NOT EXISTS users (
25
+ user_id SERIAL PRIMARY KEY,
26
+ username VARCHAR(50) UNIQUE NOT NULL,
27
+ password_hash VARCHAR(255) NOT NULL,
28
+ created_at TIMESTAMP DEFAULT NOW()
29
+ );
30
+ """))
31
+
32
+ # Table applications
33
+ conn.execute(text("""
34
+ CREATE TABLE IF NOT EXISTS applications (
35
+ app_id SERIAL PRIMARY KEY,
36
+ user_id INT REFERENCES users(user_id) ON DELETE CASCADE,
37
+ company VARCHAR(255) NOT NULL,
38
+ description TEXT,
39
+ application_date DATE NOT NULL,
40
+ end_date DATE,
41
+ expected_delay_days INT,
42
+ status VARCHAR(50) DEFAULT 'En attente',
43
+ created_at TIMESTAMP DEFAULT NOW(),
44
+ updated_at TIMESTAMP DEFAULT NOW()
45
+ );
46
+ """))
47
+
48
+ conn.commit()
database/init.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script d'initialisation de la base de données NeonDB
3
+ À lancer UNE SEULE FOIS avant le premier déploiement
4
+ """
5
+ import os
6
+ from dotenv import load_dotenv
7
+ from db_connection import init_database
8
+
9
+ load_dotenv()
10
+
11
+ if __name__ == "__main__":
12
+ print("🔧 Initialisation de la base de données NeonDB...")
13
+
14
+ # Vérification de la connection string
15
+ if not os.getenv("NEON_DB"):
16
+ print("❌ Erreur: NEON_DB n'est pas défini dans le fichier .env")
17
+ exit(1)
18
+
19
+ try:
20
+ init_database()
21
+ print("✅ Tables créées avec succès !")
22
+ print(" - Table 'users' créée")
23
+ print(" - Table 'applications' créée")
24
+ print("\n🎉 Base de données prête à l'emploi !")
25
+ except Exception as e:
26
+ print(f"❌ Erreur lors de l'initialisation : {e}")
27
+ exit(1)
database/queries.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from database.db_connection import get_db_session
2
+ from sqlalchemy import text
3
+
4
+ # ============ USER QUERIES ============
5
+
6
+ def create_user(username, password_hash):
7
+ """Crée un nouvel utilisateur"""
8
+ session = get_db_session()
9
+ try:
10
+ result = session.execute(
11
+ text("INSERT INTO users (username, password_hash) VALUES (:username, :password_hash) RETURNING user_id"),
12
+ {"username": username, "password_hash": password_hash}
13
+ )
14
+ user_id = result.fetchone()[0]
15
+ session.commit()
16
+ return user_id
17
+ except Exception as e:
18
+ session.rollback()
19
+ raise e
20
+ finally:
21
+ session.close()
22
+
23
+ def get_user_by_username(username):
24
+ """Récupère un user par son username"""
25
+ session = get_db_session()
26
+ try:
27
+ result = session.execute(
28
+ text("SELECT * FROM users WHERE username = :username"),
29
+ {"username": username}
30
+ )
31
+ user = result.fetchone()
32
+ return dict(user._mapping) if user else None
33
+ finally:
34
+ session.close()
35
+
36
+ # ============ APPLICATION QUERIES ============
37
+
38
+ def create_application(user_id, company, description, application_date, end_date, expected_delay_days):
39
+ """Crée une nouvelle candidature"""
40
+ session = get_db_session()
41
+ try:
42
+ result = session.execute(text("""
43
+ INSERT INTO applications
44
+ (user_id, company, description, application_date, end_date, expected_delay_days)
45
+ VALUES (:user_id, :company, :description, :application_date, :end_date, :expected_delay_days)
46
+ RETURNING app_id
47
+ """), {
48
+ "user_id": user_id,
49
+ "company": company,
50
+ "description": description,
51
+ "application_date": application_date,
52
+ "end_date": end_date,
53
+ "expected_delay_days": expected_delay_days
54
+ })
55
+ app_id = result.fetchone()[0]
56
+ session.commit()
57
+ return app_id
58
+ except Exception as e:
59
+ session.rollback()
60
+ raise e
61
+ finally:
62
+ session.close()
63
+
64
+ def get_user_applications(user_id):
65
+ """Récupère toutes les candidatures d'un user"""
66
+ session = get_db_session()
67
+ try:
68
+ result = session.execute(text("""
69
+ SELECT * FROM applications
70
+ WHERE user_id = :user_id
71
+ ORDER BY application_date DESC
72
+ """), {"user_id": user_id})
73
+ applications = [dict(row._mapping) for row in result.fetchall()]
74
+ return applications
75
+ finally:
76
+ session.close()
77
+
78
+ def update_application(app_id, company, description, application_date, end_date, expected_delay_days, status):
79
+ """Met à jour une candidature"""
80
+ session = get_db_session()
81
+ try:
82
+ session.execute(text("""
83
+ UPDATE applications
84
+ SET company = :company,
85
+ description = :description,
86
+ application_date = :application_date,
87
+ end_date = :end_date,
88
+ expected_delay_days = :expected_delay_days,
89
+ status = :status,
90
+ updated_at = NOW()
91
+ WHERE app_id = :app_id
92
+ """), {
93
+ "company": company,
94
+ "description": description,
95
+ "application_date": application_date,
96
+ "end_date": end_date,
97
+ "expected_delay_days": expected_delay_days,
98
+ "status": status,
99
+ "app_id": app_id
100
+ })
101
+ session.commit()
102
+ except Exception as e:
103
+ session.rollback()
104
+ raise e
105
+ finally:
106
+ session.close()
107
+
108
+ def delete_application(app_id):
109
+ """Supprime une candidature"""
110
+ session = get_db_session()
111
+ try:
112
+ session.execute(
113
+ text("DELETE FROM applications WHERE app_id = :app_id"),
114
+ {"app_id": app_id}
115
+ )
116
+ session.commit()
117
+ except Exception as e:
118
+ session.rollback()
119
+ raise e
120
+ finally:
121
+ session.close()
pages/1_📝_Nouvelle_candidature.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from datetime import date, timedelta
3
+ from utils.auth import is_logged_in
4
+ from database.queries import create_application
5
+
6
+ # Configuration de la page
7
+ st.set_page_config(
8
+ page_title="Nouvelle candidature",
9
+ page_icon="📝",
10
+ layout="wide"
11
+ )
12
+
13
+ # Vérification de l'authentification
14
+ if not is_logged_in():
15
+ st.warning("⚠️ Veuillez vous connecter pour accéder à cette page")
16
+ st.stop()
17
+
18
+ # ============ PAGE PRINCIPALE ============
19
+
20
+ st.title("📝 Nouvelle candidature")
21
+ st.markdown("### Ajoutez une nouvelle candidature à votre suivi")
22
+
23
+ # Formulaire
24
+ with st.form("new_application_form", clear_on_submit=True):
25
+ col1, col2 = st.columns(2)
26
+
27
+ with col1:
28
+ company = st.text_input(
29
+ "Entreprise *",
30
+ placeholder="Ex: Google, Jedha, RATP...",
31
+ help="Nom de l'entreprise"
32
+ )
33
+
34
+ application_date = st.date_input(
35
+ "Date de candidature *",
36
+ value=date.today(),
37
+ help="Date à laquelle vous avez envoyé votre candidature"
38
+ )
39
+
40
+ expected_delay_days = st.number_input(
41
+ "Délai de réponse attendu (jours)",
42
+ min_value=1,
43
+ max_value=365,
44
+ value=14,
45
+ help="Nombre de jours avant une relance"
46
+ )
47
+
48
+ with col2:
49
+ description = st.text_area(
50
+ "Description / Notes",
51
+ placeholder="Poste, source de l'annonce, contact, notes personnelles...",
52
+ height=100,
53
+ help="Informations complémentaires sur cette candidature"
54
+ )
55
+
56
+ end_date = st.date_input(
57
+ "Date de fin (optionnel)",
58
+ value=None,
59
+ help="Date de clôture de la candidature (si connue)"
60
+ )
61
+
62
+ st.markdown("---")
63
+
64
+ submitted = st.form_submit_button("✅ Enregistrer la candidature", type="primary", use_container_width=True)
65
+
66
+ if submitted:
67
+ # Validation
68
+ if not company:
69
+ st.error("⚠️ Le nom de l'entreprise est obligatoire")
70
+ elif not application_date:
71
+ st.error("⚠️ La date de candidature est obligatoire")
72
+ else:
73
+ try:
74
+ # Création de la candidature
75
+ app_id = create_application(
76
+ user_id=st.session_state['user_id'],
77
+ company=company,
78
+ description=description,
79
+ application_date=application_date,
80
+ end_date=end_date,
81
+ expected_delay_days=expected_delay_days
82
+ )
83
+
84
+ st.success(f"✅ Candidature chez **{company}** enregistrée avec succès !")
85
+ st.balloons()
86
+
87
+ # Info sur la date de relance
88
+ relance_date = application_date + timedelta(days=expected_delay_days)
89
+ st.info(f"📅 Date de relance suggérée : **{relance_date.strftime('%d/%m/%Y')}**")
90
+
91
+ except Exception as e:
92
+ st.error(f"❌ Erreur lors de l'enregistrement : {e}")
93
+
94
+ # ============ SECTION AIDE ============
95
+
96
+ st.markdown("---")
97
+
98
+ with st.expander("💡 Conseils pour bien suivre vos candidatures"):
99
+ st.markdown("""
100
+ **Bonnes pratiques :**
101
+
102
+ - 📋 **Description détaillée** : Notez le poste exact, le lien vers l'annonce, le nom du recruteur
103
+ - ⏰ **Délai réaliste** : 7-14 jours pour une startup, 14-30 jours pour une grande entreprise
104
+ - 🔔 **Relance** : Passé le délai, n'hésitez pas à relancer poliment
105
+ - 📊 **Statut** : Mettez à jour régulièrement le statut dans "Mes candidatures"
106
+
107
+ **Exemples de notes utiles :**
108
+ - "Candidature via LinkedIn - Contact : Jean Dupont (RH)"
109
+ - "Référé par Marie (ancienne collègue)"
110
+ - "Poste : Data Scientist Senior - Ref: DS-2025-01"
111
+ - "Entretien téléphonique prévu le 25/02"
112
+ """)
pages/2_📊_Mes_candidatures.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ from datetime import date, timedelta
4
+ from utils.auth import is_logged_in
5
+ from queries import get_user_applications, update_application, delete_application
6
+
7
+ # Configuration de la page
8
+ st.set_page_config(
9
+ page_title="Mes candidatures",
10
+ page_icon="📊",
11
+ layout="wide"
12
+ )
13
+
14
+ # Vérification de l'authentification
15
+ if not is_logged_in():
16
+ st.warning("⚠️ Veuillez vous connecter pour accéder à cette page")
17
+ st.stop()
18
+
19
+ # ============ PAGE PRINCIPALE ============
20
+
21
+ st.title("📊 Mes candidatures")
22
+ st.markdown("### Vue d'ensemble de toutes vos candidatures")
23
+
24
+ # Chargement des candidatures
25
+ try:
26
+ applications = get_user_applications(st.session_state['user_id'])
27
+ except Exception as e:
28
+ st.error(f"❌ Erreur lors du chargement des candidatures : {e}")
29
+ st.stop()
30
+
31
+ # ============ STATISTIQUES ============
32
+
33
+ if applications:
34
+ col1, col2, col3, col4 = st.columns(4)
35
+
36
+ total = len(applications)
37
+ en_attente = len([app for app in applications if app['status'] == 'En attente'])
38
+ relance = len([app for app in applications if app['status'] == 'Relancé'])
39
+ accepte = len([app for app in applications if app['status'] == 'Accepté'])
40
+ refuse = len([app for app in applications if app['status'] == 'Refusé'])
41
+
42
+ with col1:
43
+ st.metric("📋 Total", total)
44
+ with col2:
45
+ st.metric("⏳ En attente", en_attente)
46
+ with col3:
47
+ st.metric("✅ Acceptées", accepte)
48
+ with col4:
49
+ st.metric("❌ Refusées", refuse)
50
+
51
+ st.markdown("---")
52
+
53
+ # ============ FILTRES ============
54
+
55
+ if applications:
56
+ col1, col2, col3 = st.columns(3)
57
+
58
+ with col1:
59
+ status_filter = st.multiselect(
60
+ "Filtrer par statut",
61
+ options=["En attente", "Relancé", "Accepté", "Refusé"],
62
+ default=["En attente", "Relancé", "Accepté", "Refusé"]
63
+ )
64
+
65
+ with col2:
66
+ search_company = st.text_input("🔍 Rechercher une entreprise", placeholder="Nom de l'entreprise...")
67
+
68
+ with col3:
69
+ sort_by = st.selectbox(
70
+ "Trier par",
71
+ options=["Date de candidature (récent)", "Date de candidature (ancien)", "Entreprise (A-Z)", "Statut"]
72
+ )
73
+
74
+ st.markdown("---")
75
+
76
+ # ============ AFFICHAGE DES CANDIDATURES ============
77
+
78
+ if not applications:
79
+ st.info("📭 Aucune candidature enregistrée pour le moment")
80
+ st.markdown("👉 Rendez-vous sur la page **Nouvelle candidature** pour commencer !")
81
+ else:
82
+ # Filtrage
83
+ filtered_apps = [app for app in applications if app['status'] in status_filter]
84
+
85
+ if search_company:
86
+ filtered_apps = [app for app in filtered_apps if search_company.lower() in app['company'].lower()]
87
+
88
+ # Tri
89
+ if sort_by == "Date de candidature (récent)":
90
+ filtered_apps = sorted(filtered_apps, key=lambda x: x['application_date'], reverse=True)
91
+ elif sort_by == "Date de candidature (ancien)":
92
+ filtered_apps = sorted(filtered_apps, key=lambda x: x['application_date'])
93
+ elif sort_by == "Entreprise (A-Z)":
94
+ filtered_apps = sorted(filtered_apps, key=lambda x: x['company'])
95
+ elif sort_by == "Statut":
96
+ filtered_apps = sorted(filtered_apps, key=lambda x: x['status'])
97
+
98
+ # Affichage
99
+ if not filtered_apps:
100
+ st.warning("Aucune candidature ne correspond à vos filtres")
101
+ else:
102
+ st.markdown(f"**{len(filtered_apps)} candidature(s) trouvée(s)**")
103
+
104
+ for app in filtered_apps:
105
+ # Calcul de la date de relance suggérée
106
+ relance_date = app['application_date'] + timedelta(days=app['expected_delay_days'] or 14)
107
+ days_since_application = (date.today() - app['application_date']).days
108
+
109
+ # Badge de statut
110
+ status_colors = {
111
+ "En attente": "🟡",
112
+ "Relancé": "🔵",
113
+ "Accepté": "🟢",
114
+ "Refusé": "🔴"
115
+ }
116
+ status_badge = status_colors.get(app['status'], "⚪")
117
+
118
+ with st.expander(f"{status_badge} **{app['company']}** - {app['application_date'].strftime('%d/%m/%Y')}", expanded=False):
119
+ col1, col2 = st.columns([2, 1])
120
+
121
+ with col1:
122
+ st.markdown(f"**Description :**")
123
+ st.text(app['description'] or "Aucune description")
124
+
125
+ st.markdown(f"**📅 Date de candidature :** {app['application_date'].strftime('%d/%m/%Y')} ({days_since_application} jours)")
126
+
127
+ if app['end_date']:
128
+ st.markdown(f"**🏁 Date de fin :** {app['end_date'].strftime('%d/%m/%Y')}")
129
+
130
+ st.markdown(f"**⏰ Délai attendu :** {app['expected_delay_days']} jours")
131
+ st.markdown(f"**📆 Relance suggérée :** {relance_date.strftime('%d/%m/%Y')}")
132
+
133
+ # Alerte si délai dépassé
134
+ if app['status'] == "En attente" and date.today() > relance_date:
135
+ days_overdue = (date.today() - relance_date).days
136
+ st.warning(f"⚠️ Délai dépassé de {days_overdue} jour(s) - Pensez à relancer !")
137
+
138
+ with col2:
139
+ st.markdown("**Actions**")
140
+
141
+ # Modification du statut
142
+ new_status = st.selectbox(
143
+ "Statut",
144
+ options=["En attente", "Relancé", "Accepté", "Refusé"],
145
+ index=["En attente", "Relancé", "Accepté", "Refusé"].index(app['status']),
146
+ key=f"status_{app['app_id']}"
147
+ )
148
+
149
+ col_btn1, col_btn2 = st.columns(2)
150
+
151
+ with col_btn1:
152
+ if st.button("💾 Sauvegarder", key=f"save_{app['app_id']}", type="primary", use_container_width=True):
153
+ try:
154
+ update_application(
155
+ app_id=app['app_id'],
156
+ company=app['company'],
157
+ description=app['description'],
158
+ application_date=app['application_date'],
159
+ end_date=app['end_date'],
160
+ expected_delay_days=app['expected_delay_days'],
161
+ status=new_status
162
+ )
163
+ st.success("✅ Mis à jour !")
164
+ st.rerun()
165
+ except Exception as e:
166
+ st.error(f"Erreur : {e}")
167
+
168
+ with col_btn2:
169
+ if st.button("🗑️ Supprimer", key=f"delete_{app['app_id']}", type="secondary", use_container_width=True):
170
+ try:
171
+ delete_application(app['app_id'])
172
+ st.success("🗑️ Supprimée !")
173
+ st.rerun()
174
+ except Exception as e:
175
+ st.error(f"Erreur : {e}")
176
+
177
+ # ============ EXPORT ============
178
+
179
+ if applications:
180
+ st.markdown("---")
181
+
182
+ # Conversion en DataFrame pour export
183
+ df = pd.DataFrame(applications)
184
+ df['application_date'] = pd.to_datetime(df['application_date']).dt.strftime('%d/%m/%Y')
185
+ df['end_date'] = pd.to_datetime(df['end_date']).dt.strftime('%d/%m/%Y')
186
+
187
+ csv = df.to_csv(index=False).encode('utf-8')
188
+
189
+ st.download_button(
190
+ label="📥 Exporter en CSV",
191
+ data=csv,
192
+ file_name=f"candidatures_{st.session_state['username']}_{date.today()}.csv",
193
+ mime="text/csv"
194
+ )
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ sqlalchemy
3
+ psycopg2-binary
4
+ python-dotenv
5
+ bcrypt
6
+ pandas
7
+
8
+ huggingface-hub
9
+
10
+ # Dev dependencies (pour CI uniquement)
11
+ pytest
12
+ pytest-cov
13
+ flake8
utils/auth.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import bcrypt
2
+ import streamlit as st
3
+ from database.queries import get_user_by_username, create_user
4
+
5
+ def hash_password(password):
6
+ """Hash un mot de passe"""
7
+ return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
8
+
9
+ def verify_password(password, password_hash):
10
+ """Vérifie un mot de passe"""
11
+ return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
12
+
13
+ def login(username, password):
14
+ """Authentifie un utilisateur"""
15
+ user = get_user_by_username(username)
16
+ if user and verify_password(password, user['password_hash']):
17
+ st.session_state['logged_in'] = True
18
+ st.session_state['user_id'] = user['user_id']
19
+ st.session_state['username'] = user['username']
20
+ return True
21
+ return False
22
+
23
+ def logout():
24
+ """Déconnecte l'utilisateur"""
25
+ st.session_state['logged_in'] = False
26
+ st.session_state['user_id'] = None
27
+ st.session_state['username'] = None
28
+
29
+ def register(username, password):
30
+ """Enregistre un nouvel utilisateur"""
31
+ try:
32
+ password_hash = hash_password(password)
33
+ user_id = create_user(username, password_hash)
34
+ return True, "Compte créé avec succès !"
35
+ except Exception as e:
36
+ return False, f"Erreur: {str(e)}"
37
+
38
+ def is_logged_in():
39
+ """Vérifie si l'utilisateur est connecté"""
40
+ return st.session_state.get('logged_in', False)