Modification virements salaires staff
This commit is contained in:
parent
72a6b157ca
commit
47b7535bbc
7 changed files with 716 additions and 65 deletions
179
VIREMENTS_SALAIRES_LOGIQUE_CLARIFIEE.md
Normal file
179
VIREMENTS_SALAIRES_LOGIQUE_CLARIFIEE.md
Normal 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
|
||||||
|
|
@ -25,8 +25,11 @@ export async function GET(req: Request) {
|
||||||
const limit = Math.min(500, parseInt(url.searchParams.get("limit") || "100", 10));
|
const limit = Math.min(500, parseInt(url.searchParams.get("limit") || "100", 10));
|
||||||
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
|
||||||
|
|
||||||
// Build base query
|
// Build base query with organization name
|
||||||
let query = sb.from("salary_transfers").select("*", { count: "exact" });
|
let query = sb.from("salary_transfers").select(`
|
||||||
|
*,
|
||||||
|
organizations!org_id(name)
|
||||||
|
`, { count: "exact" });
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
// simple ilike search on period_label, callsheet_url, notes
|
// simple ilike search on period_label, callsheet_url, notes
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ export async function PATCH(
|
||||||
num_appel,
|
num_appel,
|
||||||
total_net,
|
total_net,
|
||||||
notes,
|
notes,
|
||||||
|
client_wire_received_at,
|
||||||
|
notification_ok,
|
||||||
|
salaires_payes,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
// 4) Build update object (only include provided fields)
|
// 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 (num_appel !== undefined) updateData.num_appel = num_appel;
|
||||||
if (total_net !== undefined) updateData.total_net = total_net;
|
if (total_net !== undefined) updateData.total_net = total_net;
|
||||||
if (notes !== undefined) updateData.notes = notes;
|
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);
|
console.log("[update salary transfer] Update data:", updateData);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export async function POST(req: NextRequest) {
|
||||||
period_label,
|
period_label,
|
||||||
deadline,
|
deadline,
|
||||||
mode,
|
mode,
|
||||||
|
num_appel,
|
||||||
total_net,
|
total_net,
|
||||||
notes,
|
notes,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
@ -79,10 +80,12 @@ export async function POST(req: NextRequest) {
|
||||||
period_label: period_label || null,
|
period_label: period_label || null,
|
||||||
deadline,
|
deadline,
|
||||||
mode,
|
mode,
|
||||||
|
num_appel: num_appel || null,
|
||||||
total_net: total_net || null,
|
total_net: total_net || null,
|
||||||
notes: notes || null,
|
notes: notes || null,
|
||||||
notification_sent: false,
|
notification_sent: false,
|
||||||
notification_ok: false,
|
notification_ok: false,
|
||||||
|
salaires_payes: false,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ function formatAmount(amount: string | number | null | undefined): string {
|
||||||
type SalaryTransfer = {
|
type SalaryTransfer = {
|
||||||
id: string;
|
id: string;
|
||||||
org_id?: string | null;
|
org_id?: string | null;
|
||||||
|
organizations?: { name: string } | null;
|
||||||
period_month?: string | null;
|
period_month?: string | null;
|
||||||
period_label?: string | null;
|
period_label?: string | null;
|
||||||
mode?: string | null;
|
mode?: string | null;
|
||||||
|
|
@ -49,6 +50,7 @@ type SalaryTransfer = {
|
||||||
notification_sent?: boolean | null;
|
notification_sent?: boolean | null;
|
||||||
notification_ok?: boolean | null;
|
notification_ok?: boolean | null;
|
||||||
client_wire_received_at?: string | null;
|
client_wire_received_at?: string | null;
|
||||||
|
salaires_payes?: boolean | null;
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
created_at?: string | null;
|
created_at?: string | null;
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
|
|
@ -84,6 +86,7 @@ export default function SalaryTransfersGrid({
|
||||||
const [notificationSentFilter, setNotificationSentFilter] = useState<string | null>(null);
|
const [notificationSentFilter, setNotificationSentFilter] = useState<string | null>(null);
|
||||||
const [notificationOkFilter, setNotificationOkFilter] = useState<string | null>(null);
|
const [notificationOkFilter, setNotificationOkFilter] = useState<string | null>(null);
|
||||||
const [hasClientWireFilter, setHasClientWireFilter] = 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 [deadlineFrom, setDeadlineFrom] = useState<string | null>(null);
|
||||||
const [deadlineTo, setDeadlineTo] = useState<string | null>(null);
|
const [deadlineTo, setDeadlineTo] = useState<string | null>(null);
|
||||||
const [sortField, setSortField] = useState<string>("period_month");
|
const [sortField, setSortField] = useState<string>("period_month");
|
||||||
|
|
@ -128,6 +131,10 @@ export default function SalaryTransfersGrid({
|
||||||
email_notifs_cc?: string | null;
|
email_notifs_cc?: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// Selection state
|
||||||
|
const [selectedTransferIds, setSelectedTransferIds] = useState<Set<string>>(new Set());
|
||||||
|
const [showBulkActionsMenu, setShowBulkActionsMenu] = useState(false);
|
||||||
|
|
||||||
// Confirmation modals
|
// Confirmation modals
|
||||||
const [showGeneratePdfConfirm, setShowGeneratePdfConfirm] = useState(false);
|
const [showGeneratePdfConfirm, setShowGeneratePdfConfirm] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
@ -216,6 +223,7 @@ export default function SalaryTransfersGrid({
|
||||||
if (notificationSentFilter) params.set('notification_sent', notificationSentFilter);
|
if (notificationSentFilter) params.set('notification_sent', notificationSentFilter);
|
||||||
if (notificationOkFilter) params.set('notification_ok', notificationOkFilter);
|
if (notificationOkFilter) params.set('notification_ok', notificationOkFilter);
|
||||||
if (hasClientWireFilter) params.set('has_client_wire', hasClientWireFilter);
|
if (hasClientWireFilter) params.set('has_client_wire', hasClientWireFilter);
|
||||||
|
if (salariesPayesFilter) params.set('salaires_payes', salariesPayesFilter);
|
||||||
if (deadlineFrom) params.set('deadline_from', deadlineFrom);
|
if (deadlineFrom) params.set('deadline_from', deadlineFrom);
|
||||||
if (deadlineTo) params.set('deadline_to', deadlineTo);
|
if (deadlineTo) params.set('deadline_to', deadlineTo);
|
||||||
params.set('sort', sortField);
|
params.set('sort', sortField);
|
||||||
|
|
@ -239,7 +247,7 @@ export default function SalaryTransfersGrid({
|
||||||
// Debounce searches when filters change
|
// Debounce searches when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if no filters applied, prefer initial data
|
// 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) {
|
if (noFilters) {
|
||||||
setRows(initialData || []);
|
setRows(initialData || []);
|
||||||
return;
|
return;
|
||||||
|
|
@ -248,7 +256,7 @@ export default function SalaryTransfersGrid({
|
||||||
const t = setTimeout(() => fetchServer(0), 300);
|
const t = setTimeout(() => fetchServer(0), 300);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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
|
// 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]);
|
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,
|
num_appel: editForm.num_appel,
|
||||||
total_net: editForm.total_net,
|
total_net: editForm.total_net,
|
||||||
notes: editForm.notes,
|
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);
|
console.log("[handleUpdateTransfer] Payload:", payload);
|
||||||
|
|
@ -569,6 +580,131 @@ export default function SalaryTransfersGrid({
|
||||||
setSendingNotification(false);
|
setSendingNotification(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -584,6 +720,93 @@ export default function SalaryTransfersGrid({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Filters */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
{/* Ligne du haut: recherche + bouton filtres */}
|
{/* Ligne du haut: recherche + bouton filtres */}
|
||||||
|
|
@ -641,6 +864,7 @@ export default function SalaryTransfersGrid({
|
||||||
setNotificationSentFilter(null);
|
setNotificationSentFilter(null);
|
||||||
setNotificationOkFilter(null);
|
setNotificationOkFilter(null);
|
||||||
setHasClientWireFilter(null);
|
setHasClientWireFilter(null);
|
||||||
|
setSalariesPayesFilter(null);
|
||||||
setDeadlineFrom(null);
|
setDeadlineFrom(null);
|
||||||
setDeadlineTo(null);
|
setDeadlineTo(null);
|
||||||
setSortField('period_month');
|
setSortField('period_month');
|
||||||
|
|
@ -676,7 +900,7 @@ export default function SalaryTransfersGrid({
|
||||||
{/* Filtre Notification OK */}
|
{/* Filtre Notification OK */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
Notification OK
|
Paiement client reçu
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={notificationOkFilter ?? ""}
|
value={notificationOkFilter ?? ""}
|
||||||
|
|
@ -689,15 +913,31 @@ export default function SalaryTransfersGrid({
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filtre Virement client reçu */}
|
{/* Filtre Virement client reçu - OBSOLETE, gardé pour compatibilité */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
Virement client reçu
|
Date virement client
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={hasClientWireFilter ?? ""}
|
value={hasClientWireFilter ?? ""}
|
||||||
onChange={(e) => setHasClientWireFilter(e.target.value || null)}
|
onChange={(e) => setHasClientWireFilter(e.target.value || null)}
|
||||||
className="w-full px-3 py-2 border rounded-lg bg-white text-sm"
|
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="">Tous</option>
|
||||||
<option value="true">Oui</option>
|
<option value="true">Oui</option>
|
||||||
|
|
@ -739,11 +979,22 @@ export default function SalaryTransfersGrid({
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-600">
|
<thead className="bg-slate-50 text-slate-600">
|
||||||
<tr>
|
<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'); }}>
|
<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' ? '▲' : '▼') : ''}
|
Période {sortField === 'period_month' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-3 py-2">N° Appel</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'); }}>
|
<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' ? '▲' : '▼') : ''}
|
Échéance {sortField === 'deadline' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -752,12 +1003,11 @@ export default function SalaryTransfersGrid({
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-3 py-2">Statut PDF</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">Notification</th>
|
||||||
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('client_wire_received_at'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
|
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('notification_ok'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
|
||||||
Virement reçu {sortField === 'client_wire_received_at' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
Paiement client {sortField === 'notification_ok' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-3 py-2">Notes</th>
|
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('salaires_payes'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
|
||||||
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('created_at'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
|
Salaires payés {sortField === 'salaires_payes' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||||
Créé le {sortField === 'created_at' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-3 py-2">Actions</th>
|
<th className="text-left px-3 py-2">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -769,6 +1019,21 @@ export default function SalaryTransfersGrid({
|
||||||
className="border-t hover:bg-slate-50 cursor-pointer transition-colors"
|
className="border-t hover:bg-slate-50 cursor-pointer transition-colors"
|
||||||
onClick={() => handleOpenDetails(r)}
|
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 */}
|
{/* Période */}
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<div className="font-medium">{r.period_label || "—"}</div>
|
<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>
|
<div className="font-mono text-sm">{r.num_appel || "—"}</div>
|
||||||
</td>
|
</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 */}
|
{/* Échéance */}
|
||||||
<td className="px-3 py-2">{formatDate(r.deadline)}</td>
|
<td className="px-3 py-2">{formatDate(r.deadline)}</td>
|
||||||
|
|
||||||
|
|
@ -811,47 +1067,38 @@ export default function SalaryTransfersGrid({
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Notification */}
|
{/* 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">
|
<td className="px-3 py-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
|
<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>
|
</span>
|
||||||
{r.notification_sent && (
|
{r.client_wire_received_at && (
|
||||||
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
|
<span className="text-xs text-slate-500">
|
||||||
r.notification_ok ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800'
|
{formatDate(r.client_wire_received_at)}
|
||||||
}`}>
|
|
||||||
{r.notification_ok ? 'OK' : 'Erreur'}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Virement client reçu */}
|
{/* Salaires payés */}
|
||||||
<td className="px-3 py-2">
|
<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 ${
|
||||||
<span className="inline-block px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
r.salaires_payes ? 'bg-blue-100 text-blue-800' : 'bg-slate-100 text-slate-600'
|
||||||
{formatDate(r.client_wire_received_at)}
|
}`}>
|
||||||
</span>
|
{r.salaires_payes ? '✓ Payés' : 'Non payés'}
|
||||||
) : (
|
</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>
|
</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>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
|
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|
@ -1250,6 +1497,82 @@ export default function SalaryTransfersGrid({
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Boutons d'action */}
|
||||||
<div className="flex gap-2 pt-4">
|
<div className="flex gap-2 pt-4">
|
||||||
<button
|
<button
|
||||||
|
|
@ -1306,29 +1629,67 @@ export default function SalaryTransfersGrid({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-50 p-4 rounded-lg">
|
<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">
|
<div className="flex gap-2 mt-2">
|
||||||
<span className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
|
<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'}
|
{selectedTransfer.notification_sent ? '✓ Envoyée' : '✗ Non envoyée'}
|
||||||
</span>
|
</span>
|
||||||
{selectedTransfer.notification_sent && (
|
</div>
|
||||||
<span className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
|
</div>
|
||||||
selectedTransfer.notification_ok ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800'
|
|
||||||
}`}>
|
<div className="bg-slate-50 p-4 rounded-lg">
|
||||||
{selectedTransfer.notification_ok ? '✓ OK' : '⚠ Erreur'}
|
<div className="flex items-center justify-between mb-2">
|
||||||
</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-50 p-4 rounded-lg">
|
<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="flex items-center justify-between mb-2">
|
||||||
<div className="font-medium text-slate-900">
|
<div className="text-xs text-slate-600">Salaires payés</div>
|
||||||
{selectedTransfer.client_wire_received_at
|
{!selectedTransfer.salaires_payes && !isEditing && (
|
||||||
? formatDate(selectedTransfer.client_wire_received_at)
|
<button
|
||||||
: "Non reçu"}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
76
supabase/migrations/add_salaires_payes_column.sql
Normal file
76
supabase/migrations/add_salaires_payes_column.sql
Normal 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;
|
||||||
23
supabase/migrations/transpose_callsheet_to_num_appel.sql
Normal file
23
supabase/migrations/transpose_callsheet_to_num_appel.sql
Normal 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;
|
||||||
Loading…
Reference in a new issue