Modification virements salaires staff

This commit is contained in:
odentas 2025-10-14 17:33:43 +02:00
parent 72a6b157ca
commit 47b7535bbc
7 changed files with 716 additions and 65 deletions

View file

@ -0,0 +1,179 @@
# Clarification de la logique des virements salaires
## Date : 14 octobre 2025
## Contexte
Les champs de la table `salary_transfers` ont été clarifiés pour mieux refléter le cycle de vie d'un virement :
## Nouvelle logique des champs
### 1. **notification_sent** (boolean)
- **Signification** : La notification a été envoyée au client
- **Valeur par défaut** : `false`
- **Quand passe à true** : Lors de l'appel à l'API `/api/staff/virements-salaires/[id]/notify-client`
- **Affichage** : Badge "✓ Envoyée" / "Non envoyée"
### 2. **notification_ok** (boolean) - NOUVELLE SIGNIFICATION
- **Ancienne signification** : Statut de la notification (OK / Erreur)
- **Nouvelle signification** : **Paiement client reçu** (Oui / Non)
- **Valeur par défaut** : `false`
- **Quand passe à true** :
- Manuellement par le staff via le modal d'édition
- Automatiquement quand on coche "Paiement client reçu"
- Via le bouton "Marquer comme reçu"
- **Affichage** : Badge "✓ Reçu" / "Non reçu"
### 3. **client_wire_received_at** (timestamp, optionnel)
- **Signification** : Date de réception du virement client
- **Valeur par défaut** : `null`
- **Quand renseigné** :
- Automatiquement à la date du jour lors de la cochage de "Paiement client reçu"
- Manuellement modifiable dans le formulaire d'édition
- Peut être effacé
- **Affichage** : Date formatée ou "—" si non renseignée
### 4. **salaires_payes** (boolean) - NOUVEAU CHAMP
- **Signification** : Les salaires ont été payés aux salariés
- **Valeur par défaut** : `false`
- **Quand passe à true** :
- Manuellement par le staff via le modal d'édition
- Via le bouton "Marquer comme payés"
- **Affichage** : Badge "✓ Payés" / "Non payés"
### 5. **num_appel** (text)
- **Signification** : Numéro d'appel de fonds (ex: "00001")
- **Correction** : Le champ est maintenant pris en compte lors de la **création** d'un virement
- **Affichage** : Police monospace dans le tableau et le modal
## Cycle de vie d'un virement
```
1. CRÉATION
└─> num_appel renseigné
└─> notification_sent = false
└─> notification_ok = false
└─> salaires_payes = false
2. GÉNÉRATION PDF
└─> callsheet_url renseigné
3. NOTIFICATION CLIENT
└─> notification_sent = true
└─> Email envoyé au client
4. RÉCEPTION PAIEMENT CLIENT
└─> notification_ok = true
└─> client_wire_received_at = date (optionnel)
5. PAIEMENT DES SALAIRES
└─> salaires_payes = true
```
## Interface utilisateur
### Affichage en lecture (Modal)
Le modal affiche maintenant 3 sections distinctes :
1. **Notification client**
- Statut : Envoyée / Non envoyée
2. **Paiement client reçu**
- Statut : Reçu / Non reçu
- Date : (si renseignée)
- Bouton rapide : "Marquer comme reçu"
3. **Salaires payés**
- Statut : Payés / Non payés
- Bouton rapide : "Marquer comme payés"
### Formulaire d'édition
Le formulaire permet maintenant de modifier :
1. **Paiement client**
- ☑ Checkbox "Paiement client reçu"
- 📅 Date de réception (optionnelle)
- ❌ Bouton pour effacer la date
2. **Salaires**
- ☑ Checkbox "Salaires payés aux salariés"
### Tableau principal
Les colonnes ont été réorganisées :
| Colonne | Contenu |
|---------|---------|
| Notification | Badge "✓ Envoyée" / "Non envoyée" |
| Paiement client | Badge "✓ Reçu" / "Non reçu" + date |
| Salaires payés | Badge "✓ Payés" / "Non payés" |
### Filtres
Nouveaux filtres disponibles :
- **Notification envoyée** : Oui / Non
- **Paiement client reçu** : Oui / Non
- **Date virement client** : Avec date / Sans date
- **Salaires payés** : Oui / Non (NOUVEAU)
## API
### Création - POST /api/staff/virements-salaires/create
**Champs pris en compte :**
- `num_appel` ✅ (corrigé)
- `salaires_payes` ✅ (nouveau, défaut: false)
### Mise à jour - PATCH /api/staff/virements-salaires/[id]
**Nouveaux champs acceptés :**
- `notification_ok` : boolean
- `salaires_payes` : boolean
## Migration base de données
Exécuter le fichier :
```bash
supabase/migrations/add_salaires_payes_column.sql
```
Ce script :
- ✅ Ajoute la colonne `salaires_payes`
- ✅ Vérifie l'existence de `num_appel`
- ✅ Ajoute des commentaires pour clarifier la logique
- ✅ Crée des index pour améliorer les performances
- ✅ Met à jour les données existantes (notification_ok pour les virements avec date)
## Corrections apportées
### ✅ 1. Notification liée à notification_sent
Avant : Confusion entre "notification envoyée" et "notification OK"
Après : `notification_sent` = notification envoyée (clair)
### ✅ 2. Virement reçu lié à notification_ok + client_wire_received_at
Avant : `notification_ok` = statut erreur/OK de la notification
Après : `notification_ok` = paiement client reçu (oui/non), `client_wire_received_at` = date (optionnelle)
### ✅ 3. Paiement modifiable avec ou sans date
Avant : Obligation de mettre une date pour marquer comme reçu
Après : Checkbox séparée + date optionnelle
### ✅ 4. Salaires payés lié à salaires_payes
Avant : Pas de champ dédié
Après : Nouveau champ `salaires_payes` (boolean)
### ✅ 5. Numéro d'appel pris en compte à la création
Avant : `num_appel` uniquement en modification
Après : `num_appel` pris en compte dès la création
## Tests recommandés
1. ✅ Créer un nouveau virement avec `num_appel`
2. ✅ Marquer le paiement client comme reçu (avec et sans date)
3. ✅ Marquer les salaires comme payés
4. ✅ Vérifier les filtres fonctionnent correctement
5. ✅ Tester les boutons rapides "Marquer comme reçu" et "Marquer comme payés"
6. ✅ Vérifier l'affichage dans le tableau principal

View file

@ -25,8 +25,11 @@ export async function GET(req: Request) {
const limit = Math.min(500, parseInt(url.searchParams.get("limit") || "100", 10));
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
// Build base query
let query = sb.from("salary_transfers").select("*", { count: "exact" });
// Build base query with organization name
let query = sb.from("salary_transfers").select(`
*,
organizations!org_id(name)
`, { count: "exact" });
if (q) {
// simple ilike search on period_label, callsheet_url, notes

View file

@ -50,6 +50,9 @@ export async function PATCH(
num_appel,
total_net,
notes,
client_wire_received_at,
notification_ok,
salaires_payes,
} = body;
// 4) Build update object (only include provided fields)
@ -64,6 +67,9 @@ export async function PATCH(
if (num_appel !== undefined) updateData.num_appel = num_appel;
if (total_net !== undefined) updateData.total_net = total_net;
if (notes !== undefined) updateData.notes = notes;
if (client_wire_received_at !== undefined) updateData.client_wire_received_at = client_wire_received_at;
if (notification_ok !== undefined) updateData.notification_ok = notification_ok;
if (salaires_payes !== undefined) updateData.salaires_payes = salaires_payes;
console.log("[update salary transfer] Update data:", updateData);

View file

@ -45,6 +45,7 @@ export async function POST(req: NextRequest) {
period_label,
deadline,
mode,
num_appel,
total_net,
notes,
} = body;
@ -79,10 +80,12 @@ export async function POST(req: NextRequest) {
period_label: period_label || null,
deadline,
mode,
num_appel: num_appel || null,
total_net: total_net || null,
notes: notes || null,
notification_sent: false,
notification_ok: false,
salaires_payes: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};

View file

@ -39,6 +39,7 @@ function formatAmount(amount: string | number | null | undefined): string {
type SalaryTransfer = {
id: string;
org_id?: string | null;
organizations?: { name: string } | null;
period_month?: string | null;
period_label?: string | null;
mode?: string | null;
@ -49,6 +50,7 @@ type SalaryTransfer = {
notification_sent?: boolean | null;
notification_ok?: boolean | null;
client_wire_received_at?: string | null;
salaires_payes?: boolean | null;
notes?: string | null;
created_at?: string | null;
updated_at?: string | null;
@ -84,6 +86,7 @@ export default function SalaryTransfersGrid({
const [notificationSentFilter, setNotificationSentFilter] = useState<string | null>(null);
const [notificationOkFilter, setNotificationOkFilter] = useState<string | null>(null);
const [hasClientWireFilter, setHasClientWireFilter] = useState<string | null>(null);
const [salariesPayesFilter, setSalariesPayesFilter] = useState<string | null>(null);
const [deadlineFrom, setDeadlineFrom] = useState<string | null>(null);
const [deadlineTo, setDeadlineTo] = useState<string | null>(null);
const [sortField, setSortField] = useState<string>("period_month");
@ -128,6 +131,10 @@ export default function SalaryTransfersGrid({
email_notifs_cc?: string | null;
} | null>(null);
// Selection state
const [selectedTransferIds, setSelectedTransferIds] = useState<Set<string>>(new Set());
const [showBulkActionsMenu, setShowBulkActionsMenu] = useState(false);
// Confirmation modals
const [showGeneratePdfConfirm, setShowGeneratePdfConfirm] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@ -216,6 +223,7 @@ export default function SalaryTransfersGrid({
if (notificationSentFilter) params.set('notification_sent', notificationSentFilter);
if (notificationOkFilter) params.set('notification_ok', notificationOkFilter);
if (hasClientWireFilter) params.set('has_client_wire', hasClientWireFilter);
if (salariesPayesFilter) params.set('salaires_payes', salariesPayesFilter);
if (deadlineFrom) params.set('deadline_from', deadlineFrom);
if (deadlineTo) params.set('deadline_to', deadlineTo);
params.set('sort', sortField);
@ -239,7 +247,7 @@ export default function SalaryTransfersGrid({
// Debounce searches when filters change
useEffect(() => {
// if no filters applied, prefer initial data
const noFilters = !q && !orgFilter && !modeFilter && !notificationSentFilter && !notificationOkFilter && !hasClientWireFilter && !deadlineFrom && !deadlineTo && sortField === 'period_month' && sortOrder === 'desc';
const noFilters = !q && !orgFilter && !modeFilter && !notificationSentFilter && !notificationOkFilter && !hasClientWireFilter && !salariesPayesFilter && !deadlineFrom && !deadlineTo && sortField === 'period_month' && sortOrder === 'desc';
if (noFilters) {
setRows(initialData || []);
return;
@ -248,7 +256,7 @@ export default function SalaryTransfersGrid({
const t = setTimeout(() => fetchServer(0), 300);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [q, orgFilter, modeFilter, notificationSentFilter, notificationOkFilter, hasClientWireFilter, deadlineFrom, deadlineTo, sortField, sortOrder, limit]);
}, [q, orgFilter, modeFilter, notificationSentFilter, notificationOkFilter, hasClientWireFilter, salariesPayesFilter, deadlineFrom, deadlineTo, sortField, sortOrder, limit]);
// derive options from initialData for simple selects
const modes = useMemo(() => Array.from(new Set((initialData || []).map((r) => r.mode).filter(Boolean) as string[])).slice(0,50), [initialData]);
@ -423,6 +431,9 @@ export default function SalaryTransfersGrid({
num_appel: editForm.num_appel,
total_net: editForm.total_net,
notes: editForm.notes,
client_wire_received_at: editForm.client_wire_received_at,
notification_ok: editForm.notification_ok,
salaires_payes: editForm.salaires_payes,
};
console.log("[handleUpdateTransfer] Payload:", payload);
@ -570,6 +581,131 @@ export default function SalaryTransfersGrid({
}
}
// Selection functions
const isAllSelected = selectedTransferIds.size > 0 && selectedTransferIds.size === rows.length;
const isSomeSelected = selectedTransferIds.size > 0 && selectedTransferIds.size < rows.length;
const toggleSelectAll = () => {
if (isAllSelected) {
setSelectedTransferIds(new Set());
} else {
setSelectedTransferIds(new Set(rows.map(r => r.id)));
}
};
const toggleSelectTransfer = (transferId: string) => {
setSelectedTransferIds(prev => {
const newSet = new Set(prev);
if (newSet.has(transferId)) {
newSet.delete(transferId);
} else {
newSet.add(transferId);
}
return newSet;
});
};
// Bulk actions
async function handleBulkUpdateNotificationSent(value: boolean) {
if (selectedTransferIds.size === 0) return;
try {
const updates = Array.from(selectedTransferIds).map(id => ({
id,
notification_sent: value
}));
// Update via API
for (const update of updates) {
await fetch(`/api/staff/virements-salaires/${update.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ notification_sent: value })
});
}
// Update local state
setRows(prev => prev.map(r =>
selectedTransferIds.has(r.id) ? { ...r, notification_sent: value } : r
));
toast.success(`${selectedTransferIds.size} virement(s) mis à jour`);
setSelectedTransferIds(new Set());
setShowBulkActionsMenu(false);
} catch (err: any) {
console.error("Bulk update error:", err);
toast.error("Erreur lors de la mise à jour groupée");
}
}
async function handleBulkUpdatePaymentReceived(value: boolean) {
if (selectedTransferIds.size === 0) return;
try {
const updates = Array.from(selectedTransferIds).map(id => ({
id,
notification_ok: value,
client_wire_received_at: value ? new Date().toISOString() : null
}));
// Update via API
for (const update of updates) {
await fetch(`/api/staff/virements-salaires/${update.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
notification_ok: value,
client_wire_received_at: value ? new Date().toISOString() : null
})
});
}
// Update local state
setRows(prev => prev.map(r =>
selectedTransferIds.has(r.id)
? { ...r, notification_ok: value, client_wire_received_at: value ? new Date().toISOString() : r.client_wire_received_at }
: r
));
toast.success(`${selectedTransferIds.size} virement(s) mis à jour`);
setSelectedTransferIds(new Set());
setShowBulkActionsMenu(false);
} catch (err: any) {
console.error("Bulk update error:", err);
toast.error("Erreur lors de la mise à jour groupée");
}
}
async function handleBulkUpdateSalariesPaid(value: boolean) {
if (selectedTransferIds.size === 0) return;
try {
// Update via API
for (const id of Array.from(selectedTransferIds)) {
await fetch(`/api/staff/virements-salaires/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ salaires_payes: value })
});
}
// Update local state
setRows(prev => prev.map(r =>
selectedTransferIds.has(r.id) ? { ...r, salaires_payes: value } : r
));
toast.success(`${selectedTransferIds.size} virement(s) mis à jour`);
setSelectedTransferIds(new Set());
setShowBulkActionsMenu(false);
} catch (err: any) {
console.error("Bulk update error:", err);
toast.error("Erreur lors de la mise à jour groupée");
}
}
return (
<div className="relative">
{/* Header avec bouton de création */}
@ -584,6 +720,93 @@ export default function SalaryTransfersGrid({
</button>
</div>
{/* Barre d'actions groupées */}
{selectedTransferIds.size > 0 && (
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-blue-900">
{selectedTransferIds.size} virement(s) sélectionné(s)
</span>
<button
onClick={() => setSelectedTransferIds(new Set())}
className="text-xs text-blue-600 hover:text-blue-800 underline"
>
Désélectionner tout
</button>
</div>
<div className="relative">
<button
onClick={() => setShowBulkActionsMenu(!showBulkActionsMenu)}
className="inline-flex items-center gap-2 px-4 py-2 bg-white border border-blue-300 rounded-lg hover:bg-blue-50 transition-colors text-sm font-medium text-blue-900"
>
Actions groupées
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showBulkActionsMenu && (
<div className="absolute right-0 mt-2 w-64 bg-white border border-slate-200 rounded-lg shadow-lg z-10">
<div className="p-2">
<div className="px-3 py-2 text-xs font-semibold text-slate-500 uppercase">
Notification
</div>
<button
onClick={() => handleBulkUpdateNotificationSent(true)}
className="w-full text-left px-3 py-2 text-sm hover:bg-slate-50 rounded"
>
Marquer comme notifiés
</button>
<button
onClick={() => handleBulkUpdateNotificationSent(false)}
className="w-full text-left px-3 py-2 text-sm hover:bg-slate-50 rounded"
>
Marquer comme non notifiés
</button>
<div className="my-2 border-t border-slate-200"></div>
<div className="px-3 py-2 text-xs font-semibold text-slate-500 uppercase">
Paiement client
</div>
<button
onClick={() => handleBulkUpdatePaymentReceived(true)}
className="w-full text-left px-3 py-2 text-sm hover:bg-slate-50 rounded"
>
Marquer comme reçus
</button>
<button
onClick={() => handleBulkUpdatePaymentReceived(false)}
className="w-full text-left px-3 py-2 text-sm hover:bg-slate-50 rounded"
>
Marquer comme non reçus
</button>
<div className="my-2 border-t border-slate-200"></div>
<div className="px-3 py-2 text-xs font-semibold text-slate-500 uppercase">
Salaires
</div>
<button
onClick={() => handleBulkUpdateSalariesPaid(true)}
className="w-full text-left px-3 py-2 text-sm hover:bg-slate-50 rounded"
>
Marquer comme payés
</button>
<button
onClick={() => handleBulkUpdateSalariesPaid(false)}
className="w-full text-left px-3 py-2 text-sm hover:bg-slate-50 rounded"
>
Marquer comme non payés
</button>
</div>
</div>
)}
</div>
</div>
)}
{/* Filters */}
<div className="mb-3">
{/* Ligne du haut: recherche + bouton filtres */}
@ -641,6 +864,7 @@ export default function SalaryTransfersGrid({
setNotificationSentFilter(null);
setNotificationOkFilter(null);
setHasClientWireFilter(null);
setSalariesPayesFilter(null);
setDeadlineFrom(null);
setDeadlineTo(null);
setSortField('period_month');
@ -676,7 +900,7 @@ export default function SalaryTransfersGrid({
{/* Filtre Notification OK */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Notification OK
Paiement client reçu
</label>
<select
value={notificationOkFilter ?? ""}
@ -689,15 +913,31 @@ export default function SalaryTransfersGrid({
</select>
</div>
{/* Filtre Virement client reçu */}
{/* Filtre Virement client reçu - OBSOLETE, gardé pour compatibilité */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Virement client reçu
Date virement client
</label>
<select
value={hasClientWireFilter ?? ""}
onChange={(e) => setHasClientWireFilter(e.target.value || null)}
className="w-full px-3 py-2 border rounded-lg bg-white text-sm"
>
<option value="">Tous</option>
<option value="true">Avec date</option>
<option value="false">Sans date</option>
</select>
</div>
{/* Filtre Salaires payés */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Salaires payés
</label>
<select
value={salariesPayesFilter ?? ""}
onChange={(e) => setSalariesPayesFilter(e.target.value || null)}
className="w-full px-3 py-2 border rounded-lg bg-white text-sm"
>
<option value="">Tous</option>
<option value="true">Oui</option>
@ -739,11 +979,22 @@ export default function SalaryTransfersGrid({
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-600">
<tr>
<th className="px-3 py-2 w-12">
<input
type="checkbox"
checked={isAllSelected}
ref={(input) => {
if (input) input.indeterminate = isSomeSelected;
}}
onChange={toggleSelectAll}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
</th>
<th className="text-left px-3 py-2">Organisation</th>
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('period_month'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Période {sortField === 'period_month' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
</th>
<th className="text-left px-3 py-2">N° Appel</th>
<th className="text-left px-3 py-2">Mode</th>
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('deadline'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Échéance {sortField === 'deadline' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
</th>
@ -752,12 +1003,11 @@ export default function SalaryTransfersGrid({
</th>
<th className="text-left px-3 py-2">Statut PDF</th>
<th className="text-left px-3 py-2">Notification</th>
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('client_wire_received_at'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Virement reçu {sortField === 'client_wire_received_at' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('notification_ok'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Paiement client {sortField === 'notification_ok' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
</th>
<th className="text-left px-3 py-2">Notes</th>
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('created_at'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Créé le {sortField === 'created_at' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('salaires_payes'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Salaires payés {sortField === 'salaires_payes' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
</th>
<th className="text-left px-3 py-2">Actions</th>
</tr>
@ -769,6 +1019,21 @@ export default function SalaryTransfersGrid({
className="border-t hover:bg-slate-50 cursor-pointer transition-colors"
onClick={() => handleOpenDetails(r)}
>
{/* Checkbox */}
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={selectedTransferIds.has(r.id)}
onChange={() => toggleSelectTransfer(r.id)}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
</td>
{/* Organisation */}
<td className="px-3 py-2">
<div className="font-medium text-sm">{r.organizations?.name || "—"}</div>
</td>
{/* Période */}
<td className="px-3 py-2">
<div className="font-medium">{r.period_label || "—"}</div>
@ -780,15 +1045,6 @@ export default function SalaryTransfersGrid({
<div className="font-mono text-sm">{r.num_appel || "—"}</div>
</td>
{/* Mode */}
<td className="px-3 py-2">
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
r.mode === 'odentas_reverse' ? 'bg-blue-100 text-blue-800' : 'bg-slate-100 text-slate-700'
}`}>
{r.mode || "—"}
</span>
</td>
{/* Échéance */}
<td className="px-3 py-2">{formatDate(r.deadline)}</td>
@ -811,47 +1067,38 @@ export default function SalaryTransfersGrid({
</td>
{/* Notification */}
<td className="px-3 py-2">
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
r.notification_sent ? 'bg-green-100 text-green-800' : 'bg-slate-100 text-slate-800'
}`}>
{r.notification_sent ? '✓ Envoyée' : 'Non envoyée'}
</span>
</td>
{/* Paiement client reçu */}
<td className="px-3 py-2">
<div className="flex flex-col gap-1">
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
r.notification_sent ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
r.notification_ok ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{r.notification_sent ? 'Envoyée' : 'Non envoyée'}
{r.notification_ok ? '✓ Reçu' : 'Non reçu'}
</span>
{r.notification_sent && (
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
r.notification_ok ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800'
}`}>
{r.notification_ok ? 'OK' : 'Erreur'}
</span>
)}
</div>
</td>
{/* Virement client reçu */}
<td className="px-3 py-2">
{r.client_wire_received_at ? (
<span className="inline-block px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
{r.client_wire_received_at && (
<span className="text-xs text-slate-500">
{formatDate(r.client_wire_received_at)}
</span>
) : (
<span className="inline-block px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Non reçu
</span>
)}
</td>
{/* Notes */}
<td className="px-3 py-2 max-w-xs">
{r.notes ? (
<div className="truncate" title={r.notes}>
{r.notes}
</div>
) : "—"}
</td>
{/* Créé le */}
<td className="px-3 py-2">{formatDate(r.created_at)}</td>
{/* Salaires payés */}
<td className="px-3 py-2">
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
r.salaires_payes ? 'bg-blue-100 text-blue-800' : 'bg-slate-100 text-slate-600'
}`}>
{r.salaires_payes ? '✓ Payés' : 'Non payés'}
</span>
</td>
{/* Actions */}
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
@ -1250,6 +1497,82 @@ export default function SalaryTransfersGrid({
/>
</div>
{/* Date de réception du virement client */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Paiement client
</label>
{/* Checkbox pour marquer comme reçu/non reçu */}
<div className="mb-3">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={editForm.notification_ok || false}
onChange={(e) => setEditForm({
...editForm,
notification_ok: e.target.checked,
client_wire_received_at: e.target.checked && !editForm.client_wire_received_at
? new Date().toISOString()
: editForm.client_wire_received_at
})}
className="w-4 h-4 text-green-600 rounded focus:ring-green-500"
/>
<span className="text-sm text-slate-700">Paiement client reçu</span>
</label>
</div>
{/* Date de réception (optionnelle) */}
<div>
<label className="block text-xs text-slate-600 mb-1">
Date de réception (optionnelle)
</label>
<div className="flex gap-2">
<input
type="date"
value={editForm.client_wire_received_at?.substring(0, 10) || ""}
onChange={(e) => setEditForm({
...editForm,
client_wire_received_at: e.target.value ? `${e.target.value}T00:00:00` : null
})}
className="flex-1 px-3 py-2 border rounded-lg text-sm"
/>
{editForm.client_wire_received_at && (
<button
type="button"
onClick={() => setEditForm({ ...editForm, client_wire_received_at: null })}
className="px-3 py-2 text-sm text-red-600 border border-red-300 rounded-lg hover:bg-red-50 transition-colors"
title="Effacer la date"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
{/* Salaires payés */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Salaires
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={editForm.salaires_payes || false}
onChange={(e) => setEditForm({
...editForm,
salaires_payes: e.target.checked
})}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="text-sm text-slate-700">Salaires payés aux salariés</span>
</label>
<p className="mt-1 text-xs text-slate-500">
Cochez cette case lorsque vous avez effectué les virements aux salariés
</p>
</div>
{/* Boutons d'action */}
<div className="flex gap-2 pt-4">
<button
@ -1306,29 +1629,67 @@ export default function SalaryTransfersGrid({
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<div className="text-xs text-slate-600 mb-1">Notifications</div>
<div className="text-xs text-slate-600 mb-1">Notification client</div>
<div className="flex gap-2 mt-2">
<span className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
selectedTransfer.notification_sent ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
selectedTransfer.notification_sent ? 'bg-green-100 text-green-800' : 'bg-slate-100 text-slate-800'
}`}>
{selectedTransfer.notification_sent ? '✓ Envoyée' : '✗ Non envoyée'}
</span>
{selectedTransfer.notification_sent && (
<span className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
selectedTransfer.notification_ok ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800'
}`}>
{selectedTransfer.notification_ok ? '✓ OK' : '⚠ Erreur'}
</span>
</div>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="text-xs text-slate-600">Paiement client reçu</div>
{!selectedTransfer.notification_ok && !isEditing && (
<button
onClick={() => {
setEditForm({
...selectedTransfer,
notification_ok: true,
client_wire_received_at: new Date().toISOString()
});
setIsEditing(true);
}}
className="text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
>
Marquer comme reçu
</button>
)}
</div>
<div className="space-y-1">
<div className={`font-medium ${selectedTransfer.notification_ok ? 'text-green-700' : 'text-slate-900'}`}>
{selectedTransfer.notification_ok ? '✓ Reçu' : '✗ Non reçu'}
</div>
{selectedTransfer.client_wire_received_at && (
<div className="text-xs text-slate-600">
Le {formatDate(selectedTransfer.client_wire_received_at)}
</div>
)}
</div>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<div className="text-xs text-slate-600 mb-1">Virement client reçu</div>
<div className="font-medium text-slate-900">
{selectedTransfer.client_wire_received_at
? formatDate(selectedTransfer.client_wire_received_at)
: "Non reçu"}
<div className="flex items-center justify-between mb-2">
<div className="text-xs text-slate-600">Salaires payés</div>
{!selectedTransfer.salaires_payes && !isEditing && (
<button
onClick={() => {
setEditForm({
...selectedTransfer,
salaires_payes: true
});
setIsEditing(true);
}}
className="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
Marquer comme payés
</button>
)}
</div>
<div className={`font-medium ${selectedTransfer.salaires_payes ? 'text-green-700' : 'text-slate-900'}`}>
{selectedTransfer.salaires_payes ? '✓ Payés' : '✗ Non payés'}
</div>
</div>

View file

@ -0,0 +1,76 @@
-- Migration pour ajouter les colonnes manquantes à salary_transfers
-- Date: 2025-01-14
-- 1. Ajouter la colonne salaires_payes si elle n'existe pas
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'salary_transfers'
AND column_name = 'salaires_payes'
) THEN
ALTER TABLE salary_transfers
ADD COLUMN salaires_payes BOOLEAN DEFAULT false;
COMMENT ON COLUMN salary_transfers.salaires_payes IS 'Indique si les salaires ont été payés aux salariés';
END IF;
END $$;
-- 2. Vérifier que la colonne num_appel existe
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'salary_transfers'
AND column_name = 'num_appel'
) THEN
ALTER TABLE salary_transfers
ADD COLUMN num_appel TEXT;
COMMENT ON COLUMN salary_transfers.num_appel IS 'Numéro d''appel de fonds (ex: 00001)';
END IF;
END $$;
-- 3. Vérifier que la colonne callsheet_date existe (pour la génération PDF)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'salary_transfers'
AND column_name = 'callsheet_date'
) THEN
ALTER TABLE salary_transfers
ADD COLUMN callsheet_date TIMESTAMP WITH TIME ZONE;
COMMENT ON COLUMN salary_transfers.callsheet_date IS 'Date de génération de la feuille d''appel';
END IF;
END $$;
-- 4. Ajouter des commentaires pour clarifier la logique des colonnes existantes
COMMENT ON COLUMN salary_transfers.notification_sent IS 'Indique si la notification a été envoyée au client';
COMMENT ON COLUMN salary_transfers.notification_ok IS 'Indique si le paiement du client a été reçu (ancien: statut de notification)';
COMMENT ON COLUMN salary_transfers.client_wire_received_at IS 'Date de réception du virement client (optionnelle)';
-- 5. Créer un index sur salaires_payes pour améliorer les performances des filtres
CREATE INDEX IF NOT EXISTS idx_salary_transfers_salaires_payes
ON salary_transfers(salaires_payes);
-- 6. Créer un index sur notification_ok pour les filtres
CREATE INDEX IF NOT EXISTS idx_salary_transfers_notification_ok
ON salary_transfers(notification_ok);
-- 7. Mettre à jour les données existantes (optionnel)
-- Si vous voulez que tous les virements avec une date de réception aient notification_ok à true
UPDATE salary_transfers
SET notification_ok = true
WHERE client_wire_received_at IS NOT NULL
AND notification_ok IS NULL;
-- Afficher un résumé
SELECT
COUNT(*) as total_transfers,
COUNT(CASE WHEN notification_sent = true THEN 1 END) as notifications_sent,
COUNT(CASE WHEN notification_ok = true THEN 1 END) as paiements_recus,
COUNT(CASE WHEN salaires_payes = true THEN 1 END) as salaires_payes,
COUNT(CASE WHEN client_wire_received_at IS NOT NULL THEN 1 END) as avec_date_reception
FROM salary_transfers;

View file

@ -0,0 +1,23 @@
-- Migration: Transposer callsheet_url vers num_appel quand ce sont des chiffres
-- Date: 2025-10-14
-- Description: Copie les valeurs numériques de callsheet_url vers num_appel
-- Mettre à jour num_appel avec les valeurs de callsheet_url qui sont uniquement des chiffres
UPDATE salary_transfers
SET num_appel = callsheet_url
WHERE
-- Vérifier que callsheet_url n'est pas null
callsheet_url IS NOT NULL
-- Vérifier que callsheet_url contient uniquement des chiffres (regex)
AND callsheet_url ~ '^[0-9]+$'
-- Optionnel: ne mettre à jour que si num_appel est vide
AND (num_appel IS NULL OR num_appel = '');
-- Afficher un résumé des enregistrements mis à jour
-- (Décommenter pour voir le résultat après exécution)
-- SELECT
-- COUNT(*) as total_updated,
-- STRING_AGG(DISTINCT id::text, ', ') as updated_ids
-- FROM salary_transfers
-- WHERE callsheet_url ~ '^[0-9]+$'
-- AND num_appel = callsheet_url;