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 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -569,6 +580,131 @@ export default function SalaryTransfersGrid({
|
|||
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 (
|
||||
<div className="relative">
|
||||
|
|
@ -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'}
|
||||
{r.client_wire_received_at && (
|
||||
<span className="text-xs text-slate-500">
|
||||
{formatDate(r.client_wire_received_at)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Virement client reçu */}
|
||||
|
||||
{/* Salaires payés */}
|
||||
<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">
|
||||
{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>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{/* 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 */}
|
||||
<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>
|
||||
|
||||
|
|
|
|||
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