Retrait des disponibilités et des limites de temps dans la journée
Disponibilités déclarées supprimées (table `availability`, migration 0022, API, éditeur de la fiche bénévole, badge de couverture du panneau d'affectation, colonnes des statistiques) ainsi que les règles OUTSIDE_AVAILABILITY, DAILY_HOURS_EXCEEDED et INSUFFICIENT_REST, les malus de disponibilité et de repos du solveur et le filtre « Surchargés » de la vue bénévoles. Seul le chevauchement reste contrôlé. La case « je suis disponible » du formulaire public est conservée. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f671bb85bf
commit
d722a81a87
@@ -16,7 +16,6 @@ import type {
|
||||
AssignmentCreate,
|
||||
AssignmentUpdate,
|
||||
AssignmentValidateResult,
|
||||
AvailabilitySet,
|
||||
BadgeFieldsSet,
|
||||
BadgeGenerateRequest,
|
||||
BadgeSizeUpdate,
|
||||
@@ -135,23 +134,6 @@ export function useShifts(positionId?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Fenêtres de disponibilité d'un bénévole. */
|
||||
export function useVolunteerAvailability(volunteerId: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ['availability', edition, volunteerId ?? null],
|
||||
queryFn: () => api.availability.list(edition, volunteerId as string),
|
||||
enabled: Boolean(volunteerId),
|
||||
});
|
||||
}
|
||||
|
||||
/** Toutes les disponibilités de l'édition (grille des besoins). */
|
||||
export function useEditionAvailability() {
|
||||
return useQuery({
|
||||
queryKey: ['availability', edition, 'all'],
|
||||
queryFn: () => api.availability.listEdition(edition),
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignments(
|
||||
params?: { volunteerId?: string; shiftId?: string },
|
||||
options?: { enabled?: boolean },
|
||||
@@ -247,7 +229,6 @@ function useInvalidate() {
|
||||
const client = useQueryClient();
|
||||
return {
|
||||
volunteers: () => client.invalidateQueries({ queryKey: ['volunteers', edition] }),
|
||||
availability: () => client.invalidateQueries({ queryKey: ['availability', edition] }),
|
||||
artists: () => client.invalidateQueries({ queryKey: ['artists', edition] }),
|
||||
accounts: () => client.invalidateQueries({ queryKey: ['accounts', edition] }),
|
||||
positions: () => client.invalidateQueries({ queryKey: ['positions', edition] }),
|
||||
@@ -408,20 +389,6 @@ export function useExportMecenatReceipts() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Remplace toutes les fenêtres de disponibilité d'un bénévole. */
|
||||
export function useReplaceAvailability() {
|
||||
const invalidate = useInvalidate();
|
||||
return useMutation({
|
||||
mutationFn: ({ volunteerId, body }: { volunteerId: string; body: AvailabilitySet }) =>
|
||||
api.availability.replace(edition, volunteerId, body),
|
||||
onSuccess: () => {
|
||||
invalidate.availability();
|
||||
// Les avertissements « hors disponibilité » de la grille en dépendent.
|
||||
invalidate.planning();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Import de bénévoles — phase 1 : analyse du fichier. */
|
||||
export function useAnalyzeImport() {
|
||||
return useMutation({
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
AssignmentCreate,
|
||||
AssignmentUpdate,
|
||||
AssignmentValidateRequest,
|
||||
AvailabilitySet,
|
||||
BadgeFieldsSet,
|
||||
BadgeGenerateRequest,
|
||||
BadgeSizeUpdate,
|
||||
@@ -172,16 +171,6 @@ export function createHttpApi(baseUrl: string): PlanningApi {
|
||||
remove: (editionId, id) =>
|
||||
request(`${base(editionId)}/accounts/${id}`, { method: 'DELETE' }),
|
||||
},
|
||||
availability: {
|
||||
list: (editionId, volunteerId) =>
|
||||
request(`${base(editionId)}/volunteers/${volunteerId}/availability`),
|
||||
replace: (editionId, volunteerId, body: AvailabilitySet) =>
|
||||
request(`${base(editionId)}/volunteers/${volunteerId}/availability`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
listEdition: (editionId) => request(`${base(editionId)}/availability`),
|
||||
},
|
||||
imports: {
|
||||
analyze: (editionId, file: File) => {
|
||||
const form = new FormData();
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
AccountRead,
|
||||
ArtistRead,
|
||||
AssignmentRead,
|
||||
AvailabilityRead,
|
||||
BadgeFieldRead,
|
||||
DayPartRead,
|
||||
EditionRead,
|
||||
@@ -67,7 +66,6 @@ export interface MockState {
|
||||
artists: ArtistRead[];
|
||||
volunteers: VolunteerRead[];
|
||||
/** volunteerId -> fenêtres de disponibilité déclarées. */
|
||||
availability: Map<string, AvailabilityRead[]>;
|
||||
positions: PositionRead[];
|
||||
shifts: ShiftRead[];
|
||||
assignments: AssignmentRead[];
|
||||
@@ -266,37 +264,10 @@ export function buildMockState(): MockState {
|
||||
);
|
||||
|
||||
const volunteers: VolunteerRead[] = [];
|
||||
const availability = new Map<string, AvailabilityRead[]>();
|
||||
let availSeq = 0;
|
||||
const window = (
|
||||
volunteerId: string,
|
||||
date: string,
|
||||
start_time: string,
|
||||
end_time: string,
|
||||
): AvailabilityRead => ({
|
||||
id: `av${availSeq++}`,
|
||||
edition_id,
|
||||
volunteer_id: volunteerId,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
created_at: NOW,
|
||||
updated_at: NOW,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 64; i += 1) {
|
||||
const first_name = FIRST_NAMES[i % FIRST_NAMES.length];
|
||||
const last_name = LAST_NAMES[(i * 7 + 3) % LAST_NAMES.length];
|
||||
const id = `v${i}`;
|
||||
// Pour ~75 % des jours : une fenêtre large 08:00 -> 02:00 le lendemain.
|
||||
const windows: AvailabilityRead[] = [];
|
||||
for (const dateStr of DAY_DATES) {
|
||||
const edge = dateStr === DAY_DATES[0] || dateStr === DAY_DATES[3];
|
||||
if (rnd() < (edge ? 0.55 : 0.8)) {
|
||||
windows.push(window(id, dateStr, '08:00:00', '02:00:00'));
|
||||
}
|
||||
}
|
||||
availability.set(id, windows);
|
||||
const pair = () => `0${Math.floor(rnd() * 90 + 10)}`.slice(-2);
|
||||
const artist_ids = artists.filter(() => rnd() < 0.25).map((a) => a.id);
|
||||
const wished_position_ids =
|
||||
@@ -354,10 +325,6 @@ export function buildMockState(): MockState {
|
||||
|
||||
const me = volunteers[0];
|
||||
me.email = demoEmail(me.first_name, me.last_name); // toujours un compte pour la démo
|
||||
availability.set(
|
||||
me.id,
|
||||
DAY_DATES.map((dateStr) => window(me.id, dateStr, '08:00:00', '02:00:00')),
|
||||
);
|
||||
NEVER_ASSIGNED.forEach((i) => {
|
||||
volunteers[i].comment = 'Inscrit, pas encore affecté';
|
||||
});
|
||||
@@ -390,10 +357,7 @@ export function buildMockState(): MockState {
|
||||
.sort((a, b) => score(b) - score(a));
|
||||
|
||||
function score(v: VolunteerRead): number {
|
||||
const available = (availability.get(v.id) ?? []).some((w) => w.date === dateStr)
|
||||
? 100
|
||||
: 0;
|
||||
return available - (totalHours.get(v.id) ?? 0) * 3;
|
||||
return -(totalHours.get(v.id) ?? 0) * 3;
|
||||
}
|
||||
|
||||
for (const v of pool.slice(0, target)) {
|
||||
@@ -510,7 +474,6 @@ export function buildMockState(): MockState {
|
||||
assignments,
|
||||
accounts,
|
||||
passwords,
|
||||
availability,
|
||||
badgeTemplate: {
|
||||
frontImage: null,
|
||||
frontImageType: null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { dayBoundaryFromDayParts, festivalDay, uncoveredMinutes } from '@/lib/planning';
|
||||
import { dayBoundaryFromDayParts, festivalDay } from '@/lib/planning';
|
||||
import { hoursBetween, naiveMinutes } from '@/lib/time';
|
||||
|
||||
import type {
|
||||
@@ -8,8 +8,6 @@ import type {
|
||||
ArtistUpdate,
|
||||
AssignmentRead,
|
||||
AssignmentValidateResult,
|
||||
AvailabilityRead,
|
||||
AvailabilitySet,
|
||||
BadgeFieldRead,
|
||||
BadgeFieldsSet,
|
||||
BadgeGenerateRequest,
|
||||
@@ -54,7 +52,6 @@ import { buildMockState, type MockPendingSubmission, type MockState } from './da
|
||||
// comportement (chargements, invalidations) proche du back-end réel.
|
||||
|
||||
const LATENCY_MS = 120;
|
||||
const EIGHT_HOURS_MIN = 8 * 60;
|
||||
const MAGIC_LINK_TTL_MS = 15 * 60 * 1000;
|
||||
|
||||
let state: MockState = buildMockState();
|
||||
@@ -199,14 +196,6 @@ const slugCode = (label: string): string =>
|
||||
/** « HH:MM » ou « HH:MM:SS » -> « HH:MM:SS ». */
|
||||
const hms = (time: string): string => (time.length === 5 ? `${time}:00` : time);
|
||||
|
||||
const sortWindows = (ws: AvailabilityRead[]): AvailabilityRead[] =>
|
||||
[...ws].sort(
|
||||
(a, b) =>
|
||||
a.volunteer_id.localeCompare(b.volunteer_id) ||
|
||||
a.date.localeCompare(b.date) ||
|
||||
a.start_time.localeCompare(b.start_time),
|
||||
);
|
||||
|
||||
/** Vœux du bénévole : dé-doublonne, ordre conservé ; 422 si un id est étranger. */
|
||||
function resolveWishes<T extends { id: string }>(
|
||||
pool: T[],
|
||||
@@ -260,7 +249,6 @@ function promotePending(pending: MockPendingSubmission): VolunteerRead {
|
||||
};
|
||||
if (pending.photo) state.photos.set(volunteer.id, pending.photo);
|
||||
state.volunteers.push(volunteer);
|
||||
state.availability.set(volunteer.id, []);
|
||||
|
||||
pending.promotedVolunteerId = volunteer.id;
|
||||
pending.status = 'PAID';
|
||||
@@ -471,63 +459,6 @@ function runValidation(volunteerId: string, candidate: ShiftRead, excludeAssignm
|
||||
}
|
||||
}
|
||||
|
||||
// OUTSIDE_AVAILABILITY (avertissement) — créneau non couvert par l'union des
|
||||
// fenêtres de disponibilité déclarées.
|
||||
const gapMin = uncoveredMinutes(
|
||||
state.availability.get(volunteerId) ?? [],
|
||||
candidate.starts_at,
|
||||
candidate.ends_at,
|
||||
);
|
||||
if (gapMin > 0) {
|
||||
violations.push({
|
||||
code: 'OUTSIDE_AVAILABILITY',
|
||||
severity: 'WARNING',
|
||||
params: {
|
||||
date: candidate.starts_at.slice(0, 10),
|
||||
uncovered_minutes: gapMin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// DAILY_HOURS_EXCEEDED (avertissement) — journée = date de début du créneau.
|
||||
const dayKey = candidate.starts_at.slice(0, 10);
|
||||
let totalMin = (naiveMinutes(candidate.ends_at) - naiveMinutes(candidate.starts_at));
|
||||
for (const other of existing) {
|
||||
if (other.starts_at.slice(0, 10) === dayKey) {
|
||||
totalMin += naiveMinutes(other.ends_at) - naiveMinutes(other.starts_at);
|
||||
}
|
||||
}
|
||||
if (totalMin > EIGHT_HOURS_MIN) {
|
||||
violations.push({
|
||||
code: 'DAILY_HOURS_EXCEEDED',
|
||||
severity: 'WARNING',
|
||||
params: { date: dayKey, total_minutes: totalMin },
|
||||
});
|
||||
}
|
||||
|
||||
// INSUFFICIENT_REST (avertissement) — < 8 h entre deux créneaux (strict).
|
||||
for (const other of existing) {
|
||||
if (overlaps(candidate, other)) continue;
|
||||
let gap: number;
|
||||
let direction: 'BEFORE' | 'AFTER';
|
||||
if (naiveMinutes(other.ends_at) <= naiveMinutes(candidate.starts_at)) {
|
||||
gap = naiveMinutes(candidate.starts_at) - naiveMinutes(other.ends_at);
|
||||
direction = 'BEFORE';
|
||||
} else if (naiveMinutes(candidate.ends_at) <= naiveMinutes(other.starts_at)) {
|
||||
gap = naiveMinutes(other.starts_at) - naiveMinutes(candidate.ends_at);
|
||||
direction = 'AFTER';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (gap < EIGHT_HOURS_MIN) {
|
||||
violations.push({
|
||||
code: 'INSUFFICIENT_REST',
|
||||
severity: 'WARNING',
|
||||
params: { other_shift_id: other.id, direction, gap_minutes: gap },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
@@ -567,10 +498,10 @@ function buildStats(): EditionStats {
|
||||
const sum = (list: VolunteerRead[]) => round(list.reduce((t, v) => t + (v.fee_amount ?? 0), 0));
|
||||
|
||||
const boundary = dayBoundaryFromDayParts(state.dayParts);
|
||||
type Row = { shifts: number; min: number; ideal: number; avail: number; who: Set<string>; assigned: number };
|
||||
type Row = { shifts: number; min: number; ideal: number; assigned: number };
|
||||
const rows = new Map<string, Row>();
|
||||
const row = (d: string) => {
|
||||
if (!rows.has(d)) rows.set(d, { shifts: 0, min: 0, ideal: 0, avail: 0, who: new Set(), assigned: 0 });
|
||||
if (!rows.has(d)) rows.set(d, { shifts: 0, min: 0, ideal: 0, assigned: 0 });
|
||||
return rows.get(d) as Row;
|
||||
};
|
||||
const shiftsById = new Map(state.shifts.map((s) => [s.id, s]));
|
||||
@@ -585,17 +516,6 @@ function buildStats(): EditionStats {
|
||||
const s = shiftsById.get(a.shift_id);
|
||||
if (s) row(festivalDay(s.starts_at, boundary)).assigned += hoursBetween(s.starts_at, s.ends_at);
|
||||
}
|
||||
const withAvailability = new Set<string>();
|
||||
for (const [vid, windows] of state.availability) {
|
||||
for (const w of windows) {
|
||||
const start = `${w.date}T${w.start_time.slice(0, 5)}:00`;
|
||||
const endDate = w.end_time > w.start_time ? w.date : addDay(w.date);
|
||||
const r = row(festivalDay(start, boundary));
|
||||
r.avail += hoursBetween(start, `${endDate}T${w.end_time.slice(0, 5)}:00`);
|
||||
r.who.add(vid);
|
||||
withAvailability.add(vid);
|
||||
}
|
||||
}
|
||||
const assignedByShift = new Map<string, number>();
|
||||
for (const a of state.assignments) assignedByShift.set(a.shift_id, (assignedByShift.get(a.shift_id) ?? 0) + 1);
|
||||
|
||||
@@ -650,8 +570,6 @@ function buildStats(): EditionStats {
|
||||
shifts: r.shifts,
|
||||
min_hours: round(r.min),
|
||||
ideal_hours: round(r.ideal),
|
||||
available_hours: round(r.avail),
|
||||
available_volunteers: r.who.size,
|
||||
assigned_hours: round(r.assigned),
|
||||
})),
|
||||
by_position: [...state.positions]
|
||||
@@ -669,7 +587,6 @@ function buildStats(): EditionStats {
|
||||
wishes: vols.filter((v) => v.wished_position_ids.includes(p.id)).length,
|
||||
};
|
||||
}),
|
||||
volunteers_with_availability: withAvailability.size,
|
||||
volunteers_with_assignment: new Set(state.assignments.map((a) => a.volunteer_id)).size,
|
||||
},
|
||||
};
|
||||
@@ -714,8 +631,6 @@ function buildMatrix(): MatrixResponse {
|
||||
const SOLVER_URGENT_BASE = 1000;
|
||||
const SOLVER_NORMAL_BASE = 20;
|
||||
const SOLVER_WISH_BONUS = 30;
|
||||
const SOLVER_AVAIL_PENALTY_PER_HALF_HOUR = 5;
|
||||
const SOLVER_REST_PENALTY = 60;
|
||||
const SOLVER_ARTIST_CONFLICT_PENALTY = 80;
|
||||
const SOLVER_FAIRNESS_PENALTY_PER_HOUR = 2;
|
||||
// Le bénévole a déjà tenu le créneau immédiatement précédent (même jour,
|
||||
@@ -829,24 +744,6 @@ function solverScore(
|
||||
}
|
||||
score += SOLVER_COMPANION_BONUS_PER_MATCH * companionsPresent;
|
||||
|
||||
const windows = state.availability.get(volunteer.id) ?? [];
|
||||
const missed = uncoveredMinutes(windows, shift.starts_at, shift.ends_at);
|
||||
score -= Math.floor(missed / 30) * SOLVER_AVAIL_PENALTY_PER_HALF_HOUR;
|
||||
|
||||
for (const other of existing) {
|
||||
if (overlaps(shift, other)) continue;
|
||||
let gap: number | null = null;
|
||||
if (naiveMinutes(other.ends_at) <= naiveMinutes(shift.starts_at)) {
|
||||
gap = naiveMinutes(shift.starts_at) - naiveMinutes(other.ends_at);
|
||||
} else if (naiveMinutes(shift.ends_at) <= naiveMinutes(other.starts_at)) {
|
||||
gap = naiveMinutes(other.starts_at) - naiveMinutes(shift.ends_at);
|
||||
}
|
||||
if (gap !== null && gap < EIGHT_HOURS_MIN) {
|
||||
score -= SOLVER_REST_PENALTY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const existingMinutes = existing.reduce(
|
||||
(sum, s) => sum + (naiveMinutes(s.ends_at) - naiveMinutes(s.starts_at)),
|
||||
0,
|
||||
@@ -1112,7 +1009,6 @@ export function createMockApi(): PlanningApi {
|
||||
updated_at: now,
|
||||
};
|
||||
state.volunteers.push(volunteer);
|
||||
state.availability.set(volunteer.id, []);
|
||||
return delay(volunteer);
|
||||
},
|
||||
update: (_edition, id, body) => {
|
||||
@@ -1154,7 +1050,6 @@ export function createMockApi(): PlanningApi {
|
||||
remove: (_edition, id) => {
|
||||
state.volunteers = state.volunteers.filter((x) => x.id !== id);
|
||||
state.assignments = state.assignments.filter((a) => a.volunteer_id !== id);
|
||||
state.availability.delete(id);
|
||||
state.photos.delete(id);
|
||||
// RGPD : effacement complet, y compris le compte de connexion éventuel
|
||||
// et les vœux de compagnon dont il était la cible.
|
||||
@@ -1256,50 +1151,6 @@ export function createMockApi(): PlanningApi {
|
||||
},
|
||||
},
|
||||
|
||||
availability: {
|
||||
list: (_edition, volunteerId) => {
|
||||
if (!state.volunteers.some((v) => v.id === volunteerId)) return notFound();
|
||||
return delay(sortWindows(state.availability.get(volunteerId) ?? []));
|
||||
},
|
||||
listEdition: () =>
|
||||
delay(
|
||||
sortWindows(
|
||||
[...state.availability.values()].flat().filter((w) =>
|
||||
state.volunteers.some((v) => v.id === w.volunteer_id),
|
||||
),
|
||||
),
|
||||
),
|
||||
replace: (_edition, volunteerId, body: AvailabilitySet) => {
|
||||
if (!state.volunteers.some((v) => v.id === volunteerId)) return notFound();
|
||||
const seen = new Set<string>();
|
||||
const windows: AvailabilityRead[] = [];
|
||||
for (const w of body.windows) {
|
||||
if (w.date < state.edition.start_date || w.date > state.edition.end_date) {
|
||||
throw new ApiError(422, 'availability.date_outside_edition');
|
||||
}
|
||||
const start = hms(w.start_time);
|
||||
const end = hms(w.end_time);
|
||||
if (start === end) throw new ApiError(422, 'availability.empty_window');
|
||||
const key = `${w.date}|${start}|${end}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const now = touch();
|
||||
windows.push({
|
||||
id: `av${state.seq++}`,
|
||||
edition_id: state.edition.id,
|
||||
volunteer_id: volunteerId,
|
||||
date: w.date,
|
||||
start_time: start,
|
||||
end_time: end,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
}
|
||||
state.availability.set(volunteerId, windows);
|
||||
return delay(sortWindows(windows));
|
||||
},
|
||||
},
|
||||
|
||||
imports: {
|
||||
analyze: async (_edition, file: File) => {
|
||||
const { headers, rows } = await readImportTable(file);
|
||||
@@ -1480,9 +1331,6 @@ export function createMockApi(): PlanningApi {
|
||||
|
||||
if (!mapping.dry_run) {
|
||||
state.volunteers = working;
|
||||
for (const v of working) {
|
||||
if (!state.availability.has(v.id)) state.availability.set(v.id, []);
|
||||
}
|
||||
}
|
||||
return delay(report);
|
||||
},
|
||||
@@ -1935,7 +1783,6 @@ export function createMockApi(): PlanningApi {
|
||||
};
|
||||
if (photo) state.photos.set(volunteer.id, photo);
|
||||
state.volunteers.push(volunteer);
|
||||
state.availability.set(volunteer.id, []);
|
||||
return delay<FormSubmissionResult>({
|
||||
volunteer_id: volunteer.id,
|
||||
pending_token: null,
|
||||
|
||||
@@ -18,11 +18,7 @@ export type AssignmentSource = 'SOLVER' | 'MANUAL';
|
||||
export type Role = 'COORDINATOR' | 'LEAD' | 'VOLUNTEER';
|
||||
export type AuthMethod = 'password' | 'magic_link';
|
||||
|
||||
export type ViolationCode =
|
||||
| 'SHIFT_OVERLAP'
|
||||
| 'OUTSIDE_AVAILABILITY'
|
||||
| 'DAILY_HOURS_EXCEEDED'
|
||||
| 'INSUFFICIENT_REST';
|
||||
export type ViolationCode = 'SHIFT_OVERLAP';
|
||||
|
||||
export type ViolationSeverity = 'BLOCKING' | 'WARNING';
|
||||
|
||||
@@ -177,32 +173,6 @@ export interface VolunteerPlanningEntry {
|
||||
referent_phone: string | null;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Availability (disponibilités déclarées par le bénévole) //
|
||||
// --------------------------------------------------------------------------- //
|
||||
export interface AvailabilityWindowInput {
|
||||
/** Date ISO (YYYY-MM-DD). */
|
||||
date: string;
|
||||
/** Heure « HH:MM » ou « HH:MM:SS ». end <= start ⇒ se termine le lendemain. */
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
}
|
||||
|
||||
export interface AvailabilitySet {
|
||||
windows: AvailabilityWindowInput[];
|
||||
}
|
||||
|
||||
export interface AvailabilityRead {
|
||||
id: string;
|
||||
edition_id: string;
|
||||
volunteer_id: string;
|
||||
date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Artist (programmation) //
|
||||
// --------------------------------------------------------------------------- //
|
||||
@@ -766,8 +736,6 @@ export interface StatsNeedDay {
|
||||
shifts: number;
|
||||
min_hours: number;
|
||||
ideal_hours: number;
|
||||
available_hours: number;
|
||||
available_volunteers: number;
|
||||
assigned_hours: number;
|
||||
}
|
||||
|
||||
@@ -801,7 +769,6 @@ export interface EditionStats {
|
||||
needs: {
|
||||
by_day: StatsNeedDay[];
|
||||
by_position: StatsNeedPosition[];
|
||||
volunteers_with_availability: number;
|
||||
volunteers_with_assignment: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import type {
|
||||
AssignmentValidateRequest,
|
||||
AssignmentValidateResult,
|
||||
AssignmentWriteResult,
|
||||
AvailabilityRead,
|
||||
AvailabilitySet,
|
||||
BadgeFieldRead,
|
||||
BadgeFieldsSet,
|
||||
BadgeGenerateRequest,
|
||||
@@ -106,18 +104,6 @@ export interface PlanningApi {
|
||||
update(editionId: string, id: string, body: AccountUpdate): Promise<AccountRead>;
|
||||
remove(editionId: string, id: string): Promise<void>;
|
||||
};
|
||||
availability: {
|
||||
/** Fenêtres déclarées par un bénévole. */
|
||||
list(editionId: string, volunteerId: string): Promise<AvailabilityRead[]>;
|
||||
/** Remplace l'intégralité des fenêtres d'un bénévole (COORDINATION). */
|
||||
replace(
|
||||
editionId: string,
|
||||
volunteerId: string,
|
||||
body: AvailabilitySet,
|
||||
): Promise<AvailabilityRead[]>;
|
||||
/** Toutes les fenêtres de l'édition (grille des besoins). */
|
||||
listEdition(editionId: string): Promise<AvailabilityRead[]>;
|
||||
};
|
||||
imports: {
|
||||
/** Phase 1 : lit le fichier, propose un mapping des colonnes. */
|
||||
analyze(editionId: string, file: File): Promise<ImportAnalyzeResponse>;
|
||||
|
||||
Reference in New Issue
Block a user