Expose ShiftKind in admin panel with Dropdown.Button picker
Shift.kind existed in the schema and on the volunteer dashboard's training filter but there was no way to create or edit a shift's kind from the admin UI — every shift landed as the default CANVASS. This wires the full loop: Backend: createShiftSchema / updateShiftSchema / listShiftsSchema and their series counterparts now accept a kind field. The shifts service passes it through on create and filters by it on list. Series shift templates propagate kind to every generated shift instance so a training series produces training shifts. Admin UI: the Create Shift button becomes a Dropdown.Button. The main action creates a Canvass shift (default); the menu offers Training, Event Staffing, Phone Bank, and Other. Each menu item pre-fills the form's kind field. A kind Select appears at the top of the form so admins can change it mid-creation or on edit. The shifts table gets a color-coded Kind column and the toolbar gets a kind filter. Bunker Admin
This commit is contained in:
parent
76fd3c7065
commit
96ff2a85d6
@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Button,
|
Button,
|
||||||
|
Dropdown,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
Tag,
|
Tag,
|
||||||
@ -40,7 +41,13 @@ import {
|
|||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
|
EnvironmentOutlined,
|
||||||
|
ReadOutlined,
|
||||||
|
CustomerServiceOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import type { MenuProps } from 'antd';
|
||||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useOutletContext, useNavigate } from 'react-router-dom';
|
import { useOutletContext, useNavigate } from 'react-router-dom';
|
||||||
@ -54,13 +61,20 @@ import type {
|
|||||||
ShiftsListParams,
|
ShiftsListParams,
|
||||||
ShiftStats,
|
ShiftStats,
|
||||||
ShiftStatus,
|
ShiftStatus,
|
||||||
|
ShiftKind,
|
||||||
Cut,
|
Cut,
|
||||||
CreateShiftSeriesInput,
|
CreateShiftSeriesInput,
|
||||||
CalendarData,
|
CalendarData,
|
||||||
EditMode,
|
EditMode,
|
||||||
RecurrenceFrequency,
|
RecurrenceFrequency,
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import { SHIFT_STATUS_COLORS, SHIFT_STATUS_LABELS, SIGNUP_SOURCE_COLORS } from '@/types/api';
|
import {
|
||||||
|
SHIFT_STATUS_COLORS,
|
||||||
|
SHIFT_STATUS_LABELS,
|
||||||
|
SIGNUP_SOURCE_COLORS,
|
||||||
|
SHIFT_KIND_LABELS,
|
||||||
|
SHIFT_KIND_COLORS,
|
||||||
|
} from '@/types/api';
|
||||||
import EditModeModal from '@/components/shifts/EditModeModal';
|
import EditModeModal from '@/components/shifts/EditModeModal';
|
||||||
import { getErrorMessage } from '@/utils/getErrorMessage';
|
import { getErrorMessage } from '@/utils/getErrorMessage';
|
||||||
import ShiftsCalendar from '@/components/shifts/ShiftsCalendar';
|
import ShiftsCalendar from '@/components/shifts/ShiftsCalendar';
|
||||||
@ -72,6 +86,18 @@ const statusOptions = Object.entries(SHIFT_STATUS_LABELS).map(([value, label]) =
|
|||||||
label,
|
label,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const kindOptions = (Object.entries(SHIFT_KIND_LABELS) as [ShiftKind, string][]).map(
|
||||||
|
([value, label]) => ({ value, label })
|
||||||
|
);
|
||||||
|
|
||||||
|
const KIND_ICONS: Record<ShiftKind, React.ReactNode> = {
|
||||||
|
CANVASS: <EnvironmentOutlined />,
|
||||||
|
TRAINING: <ReadOutlined />,
|
||||||
|
EVENT_STAFFING: <CustomerServiceOutlined />,
|
||||||
|
PHONE_BANK: <PhoneOutlined />,
|
||||||
|
OTHER: <AppstoreOutlined />,
|
||||||
|
};
|
||||||
|
|
||||||
const DAYS_OF_WEEK = [
|
const DAYS_OF_WEEK = [
|
||||||
{ label: 'Sun', value: 0 },
|
{ label: 'Sun', value: 0 },
|
||||||
{ label: 'Mon', value: 1 },
|
{ label: 'Mon', value: 1 },
|
||||||
@ -92,6 +118,7 @@ export default function ShiftsPage() {
|
|||||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||||
const searchTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const searchTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
const [statusFilter, setStatusFilter] = useState<ShiftStatus | undefined>();
|
const [statusFilter, setStatusFilter] = useState<ShiftStatus | undefined>();
|
||||||
|
const [kindFilter, setKindFilter] = useState<ShiftKind | undefined>();
|
||||||
const [stats, setStats] = useState<ShiftStats | null>(null);
|
const [stats, setStats] = useState<ShiftStats | null>(null);
|
||||||
|
|
||||||
// Create drawer
|
// Create drawer
|
||||||
@ -208,6 +235,7 @@ export default function ShiftsPage() {
|
|||||||
limit: params?.limit ?? pagination.limit,
|
limit: params?.limit ?? pagination.limit,
|
||||||
search: params?.search ?? (debouncedSearch || undefined),
|
search: params?.search ?? (debouncedSearch || undefined),
|
||||||
status: params?.status ?? statusFilter,
|
status: params?.status ?? statusFilter,
|
||||||
|
kind: params?.kind ?? kindFilter,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setShifts(data.shifts);
|
setShifts(data.shifts);
|
||||||
@ -217,13 +245,13 @@ export default function ShiftsPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [pagination.page, pagination.limit, debouncedSearch, statusFilter]);
|
}, [pagination.page, pagination.limit, debouncedSearch, statusFilter, kindFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchShifts({ page: 1 });
|
fetchShifts({ page: 1 });
|
||||||
fetchStats();
|
fetchStats();
|
||||||
fetchCuts();
|
fetchCuts();
|
||||||
}, [debouncedSearch, statusFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [debouncedSearch, statusFilter, kindFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'calendar') {
|
if (activeTab === 'calendar') {
|
||||||
@ -250,9 +278,10 @@ export default function ShiftsPage() {
|
|||||||
maxVolunteers: values.maxVolunteers,
|
maxVolunteers: values.maxVolunteers,
|
||||||
isPublic: values.isPublic || false,
|
isPublic: values.isPublic || false,
|
||||||
cutId: values.cutId || undefined,
|
cutId: values.cutId || undefined,
|
||||||
|
kind: (values.kind as ShiftKind) || 'CANVASS',
|
||||||
};
|
};
|
||||||
await api.post('/map/shifts', payload);
|
await api.post('/map/shifts', payload);
|
||||||
message.success('Shift created');
|
message.success(`${SHIFT_KIND_LABELS[payload.kind]} shift created`);
|
||||||
} else {
|
} else {
|
||||||
// Series creation
|
// Series creation
|
||||||
const seriesPayload: CreateShiftSeriesInput = {
|
const seriesPayload: CreateShiftSeriesInput = {
|
||||||
@ -264,13 +293,14 @@ export default function ShiftsPage() {
|
|||||||
maxVolunteers: values.maxVolunteers as number,
|
maxVolunteers: values.maxVolunteers as number,
|
||||||
isPublic: (values.isPublic as boolean) ?? false,
|
isPublic: (values.isPublic as boolean) ?? false,
|
||||||
cutId: values.cutId as string | undefined,
|
cutId: values.cutId as string | undefined,
|
||||||
|
kind: (values.kind as ShiftKind) || 'CANVASS',
|
||||||
frequency: values.frequency as 'DAILY' | 'WEEKLY' | 'MONTHLY',
|
frequency: values.frequency as 'DAILY' | 'WEEKLY' | 'MONTHLY',
|
||||||
daysOfWeek: values.frequency === 'WEEKLY' ? (values.daysOfWeek as number[]) : undefined,
|
daysOfWeek: values.frequency === 'WEEKLY' ? (values.daysOfWeek as number[]) : undefined,
|
||||||
startDate: dayjs(values.startDate as string).format('YYYY-MM-DD'),
|
startDate: dayjs(values.startDate as string).format('YYYY-MM-DD'),
|
||||||
endDate: values.endDate ? dayjs(values.endDate as string).format('YYYY-MM-DD') : undefined,
|
endDate: values.endDate ? dayjs(values.endDate as string).format('YYYY-MM-DD') : undefined,
|
||||||
};
|
};
|
||||||
await api.post('/map/shifts/series', seriesPayload);
|
await api.post('/map/shifts/series', seriesPayload);
|
||||||
message.success('Shift series created');
|
message.success(`${SHIFT_KIND_LABELS[seriesPayload.kind!]} series created`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCreateDrawerOpen(false);
|
setCreateDrawerOpen(false);
|
||||||
@ -305,6 +335,7 @@ export default function ShiftsPage() {
|
|||||||
if (values.isPublic !== undefined) payload.isPublic = values.isPublic;
|
if (values.isPublic !== undefined) payload.isPublic = values.isPublic;
|
||||||
if (values.status !== undefined) payload.status = values.status;
|
if (values.status !== undefined) payload.status = values.status;
|
||||||
if (values.cutId !== undefined) payload.cutId = values.cutId || null;
|
if (values.cutId !== undefined) payload.cutId = values.cutId || null;
|
||||||
|
if (values.kind !== undefined) payload.kind = values.kind;
|
||||||
|
|
||||||
await api.put(`/map/shifts/${editingShift.id}`, payload);
|
await api.put(`/map/shifts/${editingShift.id}`, payload);
|
||||||
message.success('Shift updated');
|
message.success('Shift updated');
|
||||||
@ -382,6 +413,7 @@ export default function ShiftsPage() {
|
|||||||
isPublic: shift.isPublic,
|
isPublic: shift.isPublic,
|
||||||
status: shift.status,
|
status: shift.status,
|
||||||
cutId: shift.cutId,
|
cutId: shift.cutId,
|
||||||
|
kind: shift.kind || 'CANVASS',
|
||||||
});
|
});
|
||||||
setEditDrawerOpen(true);
|
setEditDrawerOpen(true);
|
||||||
};
|
};
|
||||||
@ -502,6 +534,21 @@ export default function ShiftsPage() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Kind',
|
||||||
|
dataIndex: 'kind',
|
||||||
|
key: 'kind',
|
||||||
|
width: 130,
|
||||||
|
render: (kind: ShiftKind) => {
|
||||||
|
const k = kind || 'CANVASS';
|
||||||
|
return (
|
||||||
|
<Tag color={SHIFT_KIND_COLORS[k]} icon={KIND_ICONS[k]}>
|
||||||
|
{SHIFT_KIND_LABELS[k]}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
responsive: ['md'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
@ -620,6 +667,19 @@ export default function ShiftsPage() {
|
|||||||
|
|
||||||
const cutOptions = cuts.map((c) => ({ value: c.id, label: c.name }));
|
const cutOptions = cuts.map((c) => ({ value: c.id, label: c.name }));
|
||||||
|
|
||||||
|
const openCreateDrawer = useCallback((kind: ShiftKind = 'CANVASS') => {
|
||||||
|
createForm.resetFields();
|
||||||
|
createForm.setFieldValue('kind', kind);
|
||||||
|
setCreateDrawerOpen(true);
|
||||||
|
}, [createForm]);
|
||||||
|
|
||||||
|
const createMenuItems: MenuProps['items'] = kindOptions.map((opt) => ({
|
||||||
|
key: opt.value,
|
||||||
|
label: opt.label,
|
||||||
|
icon: KIND_ICONS[opt.value],
|
||||||
|
onClick: () => openCreateDrawer(opt.value),
|
||||||
|
}));
|
||||||
|
|
||||||
const shiftFormFields = (isEdit = false) => (
|
const shiftFormFields = (isEdit = false) => (
|
||||||
<>
|
<>
|
||||||
{/* Mode Selector (only for create, not edit) */}
|
{/* Mode Selector (only for create, not edit) */}
|
||||||
@ -637,6 +697,24 @@ export default function ShiftsPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="kind"
|
||||||
|
label="Kind"
|
||||||
|
initialValue="CANVASS"
|
||||||
|
rules={[{ required: true, message: 'Kind is required' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={kindOptions.map((opt) => ({
|
||||||
|
value: opt.value,
|
||||||
|
label: (
|
||||||
|
<Space>
|
||||||
|
{KIND_ICONS[opt.value]}
|
||||||
|
{opt.label}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="title"
|
name="title"
|
||||||
label="Title"
|
label="Title"
|
||||||
@ -897,6 +975,24 @@ export default function ShiftsPage() {
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={12} sm={6} md={4}>
|
||||||
|
<Select
|
||||||
|
placeholder="Kind"
|
||||||
|
options={kindOptions.map((opt) => ({
|
||||||
|
value: opt.value,
|
||||||
|
label: (
|
||||||
|
<Space>
|
||||||
|
{KIND_ICONS[opt.value]}
|
||||||
|
{opt.label}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
value={kindFilter}
|
||||||
|
onChange={setKindFilter}
|
||||||
|
allowClear
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
@ -918,11 +1014,18 @@ export default function ShiftsPage() {
|
|||||||
style: { cursor: 'pointer' },
|
style: { cursor: 'pointer' },
|
||||||
})}
|
})}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
locale={{ emptyText: (debouncedSearch || statusFilter)
|
locale={{ emptyText: (debouncedSearch || statusFilter || kindFilter)
|
||||||
? 'No shifts match your filters.'
|
? 'No shifts match your filters.'
|
||||||
: <div style={{ padding: 16 }}>
|
: <div style={{ padding: 16 }}>
|
||||||
<div style={{ marginBottom: 8, color: 'rgba(255,255,255,0.45)' }}>No shifts yet.</div>
|
<div style={{ marginBottom: 8, color: 'rgba(255,255,255,0.45)' }}>No shifts yet.</div>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { createForm.resetFields(); setCreateDrawerOpen(true); }}>Create Shift</Button>
|
<Dropdown.Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
menu={{ items: createMenuItems }}
|
||||||
|
onClick={() => openCreateDrawer('CANVASS')}
|
||||||
|
>
|
||||||
|
Create Shift
|
||||||
|
</Dropdown.Button>
|
||||||
</div>
|
</div>
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -955,13 +1058,14 @@ export default function ShiftsPage() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<Button
|
<Dropdown.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => setCreateDrawerOpen(true)}
|
menu={{ items: createMenuItems }}
|
||||||
|
onClick={() => openCreateDrawer('CANVASS')}
|
||||||
>
|
>
|
||||||
Create Shift
|
Create Shift
|
||||||
</Button>
|
</Dropdown.Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -813,6 +813,24 @@ export type SignupSource = 'AUTHENTICATED' | 'PUBLIC' | 'ADMIN';
|
|||||||
|
|
||||||
export type RecurrenceFrequency = 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
export type RecurrenceFrequency = 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
||||||
|
|
||||||
|
export type ShiftKind = 'CANVASS' | 'TRAINING' | 'EVENT_STAFFING' | 'PHONE_BANK' | 'OTHER';
|
||||||
|
|
||||||
|
export const SHIFT_KIND_LABELS: Record<ShiftKind, string> = {
|
||||||
|
CANVASS: 'Canvass',
|
||||||
|
TRAINING: 'Training',
|
||||||
|
EVENT_STAFFING: 'Event Staffing',
|
||||||
|
PHONE_BANK: 'Phone Bank',
|
||||||
|
OTHER: 'Other',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SHIFT_KIND_COLORS: Record<ShiftKind, string> = {
|
||||||
|
CANVASS: 'blue',
|
||||||
|
TRAINING: 'green',
|
||||||
|
EVENT_STAFFING: 'purple',
|
||||||
|
PHONE_BANK: 'orange',
|
||||||
|
OTHER: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
export interface Shift {
|
export interface Shift {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@ -824,6 +842,7 @@ export interface Shift {
|
|||||||
maxVolunteers: number;
|
maxVolunteers: number;
|
||||||
currentVolunteers: number;
|
currentVolunteers: number;
|
||||||
status: ShiftStatus;
|
status: ShiftStatus;
|
||||||
|
kind: ShiftKind;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
cutId: string | null;
|
cutId: string | null;
|
||||||
cut?: { id: string; name: string } | null;
|
cut?: { id: string; name: string } | null;
|
||||||
@ -890,6 +909,7 @@ export interface CreateShiftSeriesInput {
|
|||||||
maxVolunteers: number;
|
maxVolunteers: number;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
cutId?: string;
|
cutId?: string;
|
||||||
|
kind?: ShiftKind;
|
||||||
frequency: RecurrenceFrequency;
|
frequency: RecurrenceFrequency;
|
||||||
daysOfWeek?: number[];
|
daysOfWeek?: number[];
|
||||||
startDate: string;
|
startDate: string;
|
||||||
@ -927,6 +947,7 @@ export interface ShiftsListParams {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
status?: ShiftStatus;
|
status?: ShiftStatus;
|
||||||
|
kind?: ShiftKind;
|
||||||
upcoming?: boolean;
|
upcoming?: boolean;
|
||||||
sortBy?: 'date' | 'createdAt' | 'title';
|
sortBy?: 'date' | 'createdAt' | 'title';
|
||||||
sortOrder?: 'asc' | 'desc';
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { RecurrenceFrequency } from '@prisma/client';
|
import { RecurrenceFrequency, ShiftKind } from '@prisma/client';
|
||||||
|
|
||||||
export const createShiftSeriesSchema = z.object({
|
export const createShiftSeriesSchema = z.object({
|
||||||
title: z.string().min(1, 'Title is required'),
|
title: z.string().min(1, 'Title is required'),
|
||||||
@ -10,6 +10,7 @@ export const createShiftSeriesSchema = z.object({
|
|||||||
maxVolunteers: z.number().int().min(1, 'Must have at least 1 volunteer spot'),
|
maxVolunteers: z.number().int().min(1, 'Must have at least 1 volunteer spot'),
|
||||||
isPublic: z.boolean().optional().default(false),
|
isPublic: z.boolean().optional().default(false),
|
||||||
cutId: z.string().optional(),
|
cutId: z.string().optional(),
|
||||||
|
kind: z.nativeEnum(ShiftKind).optional().default(ShiftKind.CANVASS),
|
||||||
|
|
||||||
// Recurrence
|
// Recurrence
|
||||||
frequency: z.nativeEnum(RecurrenceFrequency),
|
frequency: z.nativeEnum(RecurrenceFrequency),
|
||||||
|
|||||||
@ -101,6 +101,7 @@ export class ShiftSeriesService {
|
|||||||
maxVolunteers: input.maxVolunteers,
|
maxVolunteers: input.maxVolunteers,
|
||||||
isPublic: input.isPublic ?? false,
|
isPublic: input.isPublic ?? false,
|
||||||
cutId: input.cutId,
|
cutId: input.cutId,
|
||||||
|
kind: input.kind,
|
||||||
seriesId: series.id,
|
seriesId: series.id,
|
||||||
createdBy,
|
createdBy,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ShiftStatus } from '@prisma/client';
|
import { ShiftStatus, ShiftKind } from '@prisma/client';
|
||||||
|
|
||||||
export const createShiftSchema = z.object({
|
export const createShiftSchema = z.object({
|
||||||
title: z.string().min(1, 'Title is required'),
|
title: z.string().min(1, 'Title is required'),
|
||||||
@ -11,6 +11,7 @@ export const createShiftSchema = z.object({
|
|||||||
maxVolunteers: z.number().int().min(1, 'Must have at least 1 volunteer spot'),
|
maxVolunteers: z.number().int().min(1, 'Must have at least 1 volunteer spot'),
|
||||||
isPublic: z.boolean().optional().default(false),
|
isPublic: z.boolean().optional().default(false),
|
||||||
cutId: z.string().optional(),
|
cutId: z.string().optional(),
|
||||||
|
kind: z.nativeEnum(ShiftKind).optional().default(ShiftKind.CANVASS),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateShiftSchema = z.object({
|
export const updateShiftSchema = z.object({
|
||||||
@ -24,6 +25,7 @@ export const updateShiftSchema = z.object({
|
|||||||
isPublic: z.boolean().optional(),
|
isPublic: z.boolean().optional(),
|
||||||
status: z.nativeEnum(ShiftStatus).optional(),
|
status: z.nativeEnum(ShiftStatus).optional(),
|
||||||
cutId: z.string().nullable().optional(),
|
cutId: z.string().nullable().optional(),
|
||||||
|
kind: z.nativeEnum(ShiftKind).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listShiftsSchema = z.object({
|
export const listShiftsSchema = z.object({
|
||||||
@ -31,6 +33,7 @@ export const listShiftsSchema = z.object({
|
|||||||
limit: z.coerce.number().int().positive().max(100).default(20),
|
limit: z.coerce.number().int().positive().max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
status: z.nativeEnum(ShiftStatus).optional(),
|
status: z.nativeEnum(ShiftStatus).optional(),
|
||||||
|
kind: z.nativeEnum(ShiftKind).optional(),
|
||||||
upcoming: z.coerce.boolean().optional(),
|
upcoming: z.coerce.boolean().optional(),
|
||||||
sortBy: z.enum(['date', 'createdAt', 'title']).optional().default('date'),
|
sortBy: z.enum(['date', 'createdAt', 'title']).optional().default('date'),
|
||||||
sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
|
sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const meetingSelect = {
|
|||||||
|
|
||||||
export const shiftsService = {
|
export const shiftsService = {
|
||||||
async findAll(filters: ListShiftsInput) {
|
async findAll(filters: ListShiftsInput) {
|
||||||
const { page, limit, search, status, upcoming, sortBy, sortOrder } = filters;
|
const { page, limit, search, status, kind, upcoming, sortBy, sortOrder } = filters;
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const where: Prisma.ShiftWhereInput = {};
|
const where: Prisma.ShiftWhereInput = {};
|
||||||
@ -52,6 +52,7 @@ export const shiftsService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status) where.status = status;
|
if (status) where.status = status;
|
||||||
|
if (kind) where.kind = kind;
|
||||||
|
|
||||||
if (upcoming) {
|
if (upcoming) {
|
||||||
where.date = { gte: new Date() };
|
where.date = { gte: new Date() };
|
||||||
@ -132,6 +133,7 @@ export const shiftsService = {
|
|||||||
maxVolunteers: data.maxVolunteers,
|
maxVolunteers: data.maxVolunteers,
|
||||||
isPublic: data.isPublic,
|
isPublic: data.isPublic,
|
||||||
cutId: data.cutId,
|
cutId: data.cutId,
|
||||||
|
kind: data.kind,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user