shift fix
This commit is contained in:
@@ -185,11 +185,19 @@ export const posUsersApi = {
|
|||||||
*
|
*
|
||||||
* Wrapped the same way — `{location_id, shifts}` (`posController.go:934-936`).
|
* Wrapped the same way — `{location_id, shifts}` (`posController.go:934-936`).
|
||||||
*/
|
*/
|
||||||
shifts: (tenantid: number, locationid: number) =>
|
/**
|
||||||
|
* The tenant's shift windows.
|
||||||
|
*
|
||||||
|
* `locationid` is optional and usually omitted. A shift belongs to the
|
||||||
|
* business, so the tenant's set is what every picker in the console should
|
||||||
|
* offer; naming a branch narrows to the tenant's plus that branch's own, for
|
||||||
|
* the outlet that genuinely runs different hours.
|
||||||
|
*/
|
||||||
|
shifts: (tenantid: number, locationid?: number) =>
|
||||||
api
|
api
|
||||||
.get<{ location_id?: number; shifts?: StaffShift[] }>(`${WEB}/tenants/getstaffshifts`, {
|
.get<{ location_id?: number; shifts?: StaffShift[] }>(`${WEB}/tenants/getstaffshifts`, {
|
||||||
tenantid,
|
tenantid,
|
||||||
locationid,
|
...(locationid ? { locationid } : {}),
|
||||||
})
|
})
|
||||||
.then((page) => (Array.isArray(page?.shifts) ? page.shifts : [])),
|
.then((page) => (Array.isArray(page?.shifts) ? page.shifts : [])),
|
||||||
|
|
||||||
@@ -207,9 +215,16 @@ export const posUsersApi = {
|
|||||||
*/
|
*/
|
||||||
createShift: (
|
createShift: (
|
||||||
tenantid: number,
|
tenantid: number,
|
||||||
locationid: number,
|
|
||||||
shift: { name: string; start_time: string; end_time: string; weekdays?: string },
|
shift: { name: string; start_time: string; end_time: string; weekdays?: string },
|
||||||
) => api.post<StaffShift>(`${WEB}/tenants/createstaffshift`, { tenantid, locationid, ...shift }),
|
/** Only for a shop that genuinely runs different hours from the business. */
|
||||||
|
locationid?: number,
|
||||||
|
) =>
|
||||||
|
api.post<StaffShift>(`${WEB}/tenants/createstaffshift`, {
|
||||||
|
tenantid,
|
||||||
|
// Zero means the whole tenant, which is the ordinary case.
|
||||||
|
locationid: locationid ?? 0,
|
||||||
|
...shift,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, type FormEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Clock, Copy, MapPin } from 'lucide-react';
|
import { Copy, MapPin } from 'lucide-react';
|
||||||
import { errorMessage } from '@/api/client';
|
import { errorMessage } from '@/api/client';
|
||||||
import { normaliseMobile, posUsersApi, staffApi, weekdayLabel } from '@/api/people';
|
import { normaliseMobile, posUsersApi, staffApi, weekdayLabel } from '@/api/people';
|
||||||
import type { PosRole, PosUser, StaffInfo, StaffShift, TenantLocation } from '@/api/types';
|
import type { PosRole, PosUser, StaffInfo, StaffShift, TenantLocation } from '@/api/types';
|
||||||
@@ -333,39 +333,6 @@ export function TillDrawer({
|
|||||||
/** The credential the backend hands back on create. Shown once, never re-read. */
|
/** The credential the backend hands back on create. Shown once, never re-read. */
|
||||||
const [issued, setIssued] = useState<string | null>(null);
|
const [issued, setIssued] = useState<string | null>(null);
|
||||||
|
|
||||||
/*
|
|
||||||
* Opening a shift window, from the form that needs one.
|
|
||||||
*
|
|
||||||
* `createstaffshift` has existed on the server since till staff were built and
|
|
||||||
* nothing in the console ever called it — so `getstaffshifts` answered
|
|
||||||
* `{"shifts": []}` at every branch, and this drawer's picker offered "Any
|
|
||||||
* shift" and nothing else, for every cashier, permanently. The same gap the
|
|
||||||
* riders had: a read wired up, a write that only the database could reach.
|
|
||||||
*
|
|
||||||
* Here rather than on a settings screen because this is where it is missed.
|
|
||||||
*/
|
|
||||||
const [newShift, setNewShift] = useState({ name: '', start_time: '09:00', end_time: '17:00' });
|
|
||||||
const setShiftField = (field: keyof typeof newShift) => (value: string) =>
|
|
||||||
setNewShift((prev) => ({ ...prev, [field]: value }));
|
|
||||||
|
|
||||||
const addShift = useMutation({
|
|
||||||
mutationFn: () =>
|
|
||||||
posUsersApi.createShift(tenantid, locationid, {
|
|
||||||
name: newShift.name.trim(),
|
|
||||||
start_time: newShift.start_time.trim(),
|
|
||||||
end_time: newShift.end_time.trim(),
|
|
||||||
}),
|
|
||||||
onSuccess: async (shift) => {
|
|
||||||
setProblem(null);
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKeys.people.shifts(tenantid, locationid) });
|
|
||||||
// Selected straight away — somebody who just typed these hours meant to
|
|
||||||
// put this person on them.
|
|
||||||
if (shift?.staff_shift_id) setShiftId(String(shift.staff_shift_id));
|
|
||||||
},
|
|
||||||
onError: (cause) => setProblem(errorMessage(cause)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasNoShifts = shifts.length === 0;
|
|
||||||
|
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -552,43 +519,6 @@ export function TillDrawer({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{/* The way out of the dead end. Only when the branch genuinely has none
|
|
||||||
— a shift creator on a screen that already has shifts is how you end
|
|
||||||
up with four overlapping windows nobody can tell apart. */}
|
|
||||||
{hasNoShifts ? (
|
|
||||||
<>
|
|
||||||
<Note>
|
|
||||||
No shift windows exist at {branchName || 'this branch'} yet, so there is nothing to
|
|
||||||
put anyone on. Name one and give its hours — every till account here can then use it.
|
|
||||||
</Note>
|
|
||||||
<Field
|
|
||||||
label="Name"
|
|
||||||
isRequired
|
|
||||||
description="What people call it — Morning, Evening, Weekend cover."
|
|
||||||
>
|
|
||||||
<TextField value={newShift.name} onChange={setShiftField('name')} />
|
|
||||||
</Field>
|
|
||||||
<FieldRow>
|
|
||||||
<Field label="Starts" isRequired>
|
|
||||||
<TextField value={newShift.start_time} onChange={setShiftField('start_time')} />
|
|
||||||
</Field>
|
|
||||||
<Field label="Ends" isRequired>
|
|
||||||
<TextField value={newShift.end_time} onChange={setShiftField('end_time')} />
|
|
||||||
</Field>
|
|
||||||
</FieldRow>
|
|
||||||
<DrawerButton
|
|
||||||
label={addShift.isPending ? 'Adding…' : 'Add this shift'}
|
|
||||||
icon={<Clock size={15} />}
|
|
||||||
isDisabled={
|
|
||||||
addShift.isPending ||
|
|
||||||
newShift.name.trim() === '' ||
|
|
||||||
newShift.start_time.trim() === '' ||
|
|
||||||
newShift.end_time.trim() === ''
|
|
||||||
}
|
|
||||||
onClick={() => addShift.mutate()}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Access">
|
<Section title="Access">
|
||||||
@@ -610,3 +540,108 @@ export function TillDrawer({
|
|||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Shift windows ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opening a shift window for the whole business.
|
||||||
|
*
|
||||||
|
* ── Why this is tenant-wide and not per branch ──────────────────────────────
|
||||||
|
*
|
||||||
|
* A shift is a fact about how a business runs, not about one of its shops. A
|
||||||
|
* tenant working 07:00–15:00 and 15:00–23:00 works those hours everywhere it
|
||||||
|
* trades, so asking for them per outlet means entering the same two windows
|
||||||
|
* three times — and the third branch quietly ends up on 07:00–15:30 with a
|
||||||
|
* cashier filed under hours that do not exist anywhere else.
|
||||||
|
*
|
||||||
|
* So the server takes `locationid = 0` for "the whole tenant", and this never
|
||||||
|
* sends one. A shop that genuinely runs different hours is a real case and the
|
||||||
|
* column still supports it; it is not what this form is for.
|
||||||
|
*
|
||||||
|
* ── Why it lives on Users & Access ─────────────────────────────────────────
|
||||||
|
*
|
||||||
|
* It was inside the till-account drawer, which put a piece of business-wide
|
||||||
|
* setup inside a form about one cashier — and meant nobody could see the list
|
||||||
|
* without pretending to edit somebody. It is a list, so it belongs beside the
|
||||||
|
* other lists of people.
|
||||||
|
*/
|
||||||
|
export function ShiftDrawer({
|
||||||
|
tenantid,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
tenantid: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [startTime, setStartTime] = useState('09:00');
|
||||||
|
const [endTime, setEndTime] = useState('17:00');
|
||||||
|
const [problem, setProblem] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const save = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
posUsersApi.createShift(tenantid, {
|
||||||
|
name: name.trim(),
|
||||||
|
start_time: startTime.trim(),
|
||||||
|
end_time: endTime.trim(),
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
// Every picker in the console reads this list, so all of them are stale
|
||||||
|
// the moment a shift is added — including the ones open behind this sheet.
|
||||||
|
await queryClient.invalidateQueries({ queryKey: queryKeys.people.all });
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (cause) => setProblem(errorMessage(cause)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const canSave =
|
||||||
|
name.trim() !== '' && startTime.trim() !== '' && endTime.trim() !== '' && !save.isPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title="Add a shift"
|
||||||
|
subtitle="Available to every branch and every account"
|
||||||
|
width={440}
|
||||||
|
onClose={onClose}
|
||||||
|
isFooterSpread
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<DrawerButton label="Cancel" variant="ghost" onClick={onClose} />
|
||||||
|
<DrawerButton
|
||||||
|
label={save.isPending ? 'Adding…' : 'Add shift'}
|
||||||
|
variant="primary"
|
||||||
|
isDisabled={!canSave}
|
||||||
|
onClick={() => save.mutate()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Note>
|
||||||
|
A shift is the window somebody works. It is informational — nothing refuses a bill rung
|
||||||
|
outside it — and every till account and rider form in this business can use it once it
|
||||||
|
exists.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<Section title="The window">
|
||||||
|
<Field
|
||||||
|
label="Name"
|
||||||
|
isRequired
|
||||||
|
description="What people call it — Morning, Evening, Weekend cover."
|
||||||
|
>
|
||||||
|
<TextField value={name} onChange={setName} />
|
||||||
|
</Field>
|
||||||
|
<FieldRow>
|
||||||
|
<Field label="Starts" isRequired description="24-hour, as HH:MM.">
|
||||||
|
<TextField value={startTime} onChange={setStartTime} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Ends" isRequired description="Before the start means it crosses midnight.">
|
||||||
|
<TextField value={endTime} onChange={setEndTime} />
|
||||||
|
</Field>
|
||||||
|
</FieldRow>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{problem ? <Alert tone="error">{problem}</Alert> : null}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { HStack } from '@astryxdesign/core/HStack';
|
|||||||
import { Selector } from '@astryxdesign/core/Selector';
|
import { Selector } from '@astryxdesign/core/Selector';
|
||||||
import { Text } from '@astryxdesign/core/Text';
|
import { Text } from '@astryxdesign/core/Text';
|
||||||
import { VStack } from '@astryxdesign/core/VStack';
|
import { VStack } from '@astryxdesign/core/VStack';
|
||||||
import { Bike, Monitor, Plus, UserCog, Users } from 'lucide-react';
|
import { Bike, Clock, Monitor, Plus, UserCog, Users } from 'lucide-react';
|
||||||
import { SearchInput } from '@/components/SearchInput';
|
import { SearchInput } from '@/components/SearchInput';
|
||||||
import type { RiderRosterRow } from '@/api/deliveries';
|
import type { RiderRosterRow } from '@/api/deliveries';
|
||||||
import type { PosUser, StaffInfo } from '@/api/types';
|
import type { PosUser, StaffInfo, StaffShift } from '@/api/types';
|
||||||
import { PageHeader } from '@/components/PageHeader';
|
import { PageHeader } from '@/components/PageHeader';
|
||||||
import { Tab, TabBar } from '@/components/TabBar';
|
import { Tab, TabBar } from '@/components/TabBar';
|
||||||
import { SectionHeader } from '@/components/SectionHeader';
|
import { SectionHeader } from '@/components/SectionHeader';
|
||||||
@@ -21,14 +21,15 @@ import {
|
|||||||
} from '@/queries/hooks';
|
} from '@/queries/hooks';
|
||||||
import { useBranchScope } from '../BranchScope';
|
import { useBranchScope } from '../BranchScope';
|
||||||
import { branchLabel } from '../format';
|
import { branchLabel } from '../format';
|
||||||
import { PersonDrawer, TillDrawer } from '../PeopleDrawers';
|
import { PersonDrawer, ShiftDrawer, TillDrawer } from '../PeopleDrawers';
|
||||||
|
import { weekdayLabel } from '@/api/people';
|
||||||
import { RiderDrawer } from '../RiderDrawer';
|
import { RiderDrawer } from '../RiderDrawer';
|
||||||
import { isUnplaced } from '../staffPlacement';
|
import { isUnplaced } from '../staffPlacement';
|
||||||
import { Panel } from '@/components/Panel';
|
import { Panel } from '@/components/Panel';
|
||||||
import { TablePager } from '@/components/TablePager';
|
import { TablePager } from '@/components/TablePager';
|
||||||
import { usePaged } from '@/components/usePaged';
|
import { usePaged } from '@/components/usePaged';
|
||||||
|
|
||||||
type Group = 'store' | 'till' | 'rider';
|
type Group = 'store' | 'till' | 'rider' | 'shift';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Users & access — three account systems, side by side and never merged.
|
* Users & access — three account systems, side by side and never merged.
|
||||||
@@ -58,13 +59,14 @@ export function UsersPage() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [editingRider, setEditingRider] = useState<RiderRosterRow | 'new' | null>(null);
|
const [editingRider, setEditingRider] = useState<RiderRosterRow | 'new' | null>(null);
|
||||||
|
const [isAddingShift, setAddingShift] = useState(false);
|
||||||
|
|
||||||
const branchIds = useMemo(() => scoped.map((branch) => branch.locationid), [scoped]);
|
const branchIds = useMemo(() => scoped.map((branch) => branch.locationid), [scoped]);
|
||||||
|
|
||||||
const staff = useStaff(tenantid || undefined);
|
const staff = useStaff(tenantid || undefined);
|
||||||
const posUserPages = usePosUsersByBranch(tenantid || undefined, branchIds);
|
const posUserPages = usePosUsersByBranch(tenantid || undefined, branchIds);
|
||||||
const roles = usePosRoles();
|
const roles = usePosRoles();
|
||||||
const shifts = useStaffShifts(tenantid || undefined, scoped[0]?.locationid);
|
const shifts = useStaffShifts(tenantid || undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The rider directory.
|
* The rider directory.
|
||||||
@@ -151,7 +153,9 @@ export function UsersPage() {
|
|||||||
? 'Add person'
|
? 'Add person'
|
||||||
: group === 'rider'
|
: group === 'rider'
|
||||||
? 'Add rider'
|
? 'Add rider'
|
||||||
: 'Add terminal account'
|
: group === 'shift'
|
||||||
|
? 'Add shift'
|
||||||
|
: 'Add terminal account'
|
||||||
}
|
}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -159,6 +163,7 @@ export function UsersPage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (group === 'store') setEditingPerson('new');
|
if (group === 'store') setEditingPerson('new');
|
||||||
else if (group === 'rider') setEditingRider('new');
|
else if (group === 'rider') setEditingRider('new');
|
||||||
|
else if (group === 'shift') setAddingShift(true);
|
||||||
else {
|
else {
|
||||||
const locationid = selected ?? scoped[0]?.locationid;
|
const locationid = selected ?? scoped[0]?.locationid;
|
||||||
if (locationid) setEditingTill({ row: 'new', locationid });
|
if (locationid) setEditingTill({ row: 'new', locationid });
|
||||||
@@ -203,6 +208,21 @@ export function UsersPage() {
|
|||||||
setRoleFilter('');
|
setRoleFilter('');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* Not a fourth kind of account — a list the other three read from.
|
||||||
|
It sits here because it is business-wide setup that every one of
|
||||||
|
them offers, and because it used to be buried inside the till
|
||||||
|
drawer, where nobody could see the list without pretending to
|
||||||
|
edit a cashier. */}
|
||||||
|
<Tab
|
||||||
|
label="Shifts"
|
||||||
|
icon={<Clock size={14} />}
|
||||||
|
count={shifts.data?.length || undefined}
|
||||||
|
isActive={group === 'shift'}
|
||||||
|
onClick={() => {
|
||||||
|
setGroup('shift');
|
||||||
|
setRoleFilter('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -210,12 +230,18 @@ export function UsersPage() {
|
|||||||
<VStack gap={1.5}>
|
<VStack gap={1.5}>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title={
|
title={
|
||||||
group === 'store' ? 'Back office' : group === 'rider' ? 'Riders' : 'Counter staff'
|
group === 'store'
|
||||||
|
? 'Back office'
|
||||||
|
: group === 'rider'
|
||||||
|
? 'Riders'
|
||||||
|
: group === 'shift'
|
||||||
|
? 'Shift windows'
|
||||||
|
: 'Counter staff'
|
||||||
}
|
}
|
||||||
action={
|
action={
|
||||||
<HStack gap={1} wrap="wrap">
|
<HStack gap={1} wrap="wrap">
|
||||||
{/* Riders have no role to filter by — a rider is a rider. */}
|
{/* Riders have no role to filter by — a rider is a rider. */}
|
||||||
{group !== 'rider' ? (
|
{group !== 'rider' && group !== 'shift' ? (
|
||||||
<div style={{ width: 170 }}>
|
<div style={{ width: 170 }}>
|
||||||
<Selector
|
<Selector
|
||||||
label="Role"
|
label="Role"
|
||||||
@@ -239,7 +265,9 @@ export function UsersPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{group === 'store' ? (
|
{group === 'shift' ? (
|
||||||
|
<ShiftTable rows={shifts.data ?? []} isLoading={shifts.isLoading} />
|
||||||
|
) : group === 'store' ? (
|
||||||
<StaffTable
|
<StaffTable
|
||||||
rows={staffRows}
|
rows={staffRows}
|
||||||
isLoading={staff.isLoading}
|
isLoading={staff.isLoading}
|
||||||
@@ -298,6 +326,12 @@ export function UsersPage() {
|
|||||||
onClose={() => setEditingRider(null)}
|
onClose={() => setEditingRider(null)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* No `row` and no branch: a shift is created for the business, and the
|
||||||
|
three account tabs beside this one all read the same list. */}
|
||||||
|
{isAddingShift ? (
|
||||||
|
<ShiftDrawer tenantid={tenantid} onClose={() => setAddingShift(false)} />
|
||||||
|
) : null}
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -749,3 +783,106 @@ function Td({
|
|||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tenant's shift windows.
|
||||||
|
*
|
||||||
|
* Read-only here. A shift is created from the header button and is used by the
|
||||||
|
* three tabs beside this one — it is not a fourth kind of account, it is the
|
||||||
|
* list they all pick from, which is why it earns a tab rather than a settings
|
||||||
|
* page nobody would find.
|
||||||
|
*/
|
||||||
|
function ShiftTable({ rows, isLoading }: { rows: StaffShift[]; isLoading: boolean }) {
|
||||||
|
const paged = usePaged(rows);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card padding={0}>
|
||||||
|
<VStack padding={3}>
|
||||||
|
<Text type="body" size="sm" color="secondary">
|
||||||
|
Reading shifts…
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card padding={0}>
|
||||||
|
<VStack gap={1} padding={4} align="center">
|
||||||
|
<span style={{ color: 'var(--color-ink-4)' }}>
|
||||||
|
<Clock size={22} />
|
||||||
|
</span>
|
||||||
|
<Text type="label" size="sm" weight="semibold">
|
||||||
|
No shifts yet
|
||||||
|
</Text>
|
||||||
|
<Text type="body" size="sm" color="secondary" style={{ textAlign: 'center', maxWidth: 420 }}>
|
||||||
|
A shift is the window somebody works. Add one and every till account and rider in this
|
||||||
|
business can be put on it — you do not set them up per branch.
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
title="Shift windows"
|
||||||
|
count={rows.length}
|
||||||
|
countLabel="shifts"
|
||||||
|
footer={<TablePager paged={paged} label="shifts" />}
|
||||||
|
>
|
||||||
|
<div className="table-scroll">
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<Th>Shift</Th>
|
||||||
|
<Th>Hours</Th>
|
||||||
|
<Th>Days</Th>
|
||||||
|
<Th>Applies to</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{paged.rows.map((shift) => (
|
||||||
|
<tr key={shift.staff_shift_id}>
|
||||||
|
<Td>
|
||||||
|
<strong style={{ display: 'block' }}>
|
||||||
|
{shift.name?.trim() || `Shift ${shift.staff_shift_id}`}
|
||||||
|
</strong>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<strong style={{ display: 'block', fontVariantNumeric: 'tabular-nums' }}>
|
||||||
|
{shift.start_time} – {shift.end_time}
|
||||||
|
</strong>
|
||||||
|
{/* A window whose end is before its start runs overnight. Said
|
||||||
|
here because the two times alone read like a typo. */}
|
||||||
|
{shift.end_time && shift.start_time && shift.end_time < shift.start_time ? (
|
||||||
|
<span style={{ fontSize: 11.5, color: 'var(--color-ink-4)' }}>
|
||||||
|
Crosses midnight
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</Td>
|
||||||
|
<Td isMuted>{weekdayLabel(shift.weekdays)}</Td>
|
||||||
|
<Td isMuted>
|
||||||
|
{/* Zero means the whole business, which is the ordinary case
|
||||||
|
and the reason this list is not per branch. */}
|
||||||
|
{shift.locationid ? 'One branch' : 'Every branch'}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Chip
|
||||||
|
label={shift.status || 'Active'}
|
||||||
|
tone={
|
||||||
|
(shift.status ?? 'Active').toLowerCase() === 'active' ? 'success' : 'warning'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function StoreStaffPage() {
|
|||||||
// Inactive included on purpose — see the note above.
|
// Inactive included on purpose — see the note above.
|
||||||
const pages = usePosUsersByBranch(tenantid || undefined, locationid ? [locationid] : [], true);
|
const pages = usePosUsersByBranch(tenantid || undefined, locationid ? [locationid] : [], true);
|
||||||
const roles = usePosRoles();
|
const roles = usePosRoles();
|
||||||
const shifts = useStaffShifts(tenantid || undefined, locationid);
|
const shifts = useStaffShifts(tenantid || undefined);
|
||||||
|
|
||||||
const page = pages[0];
|
const page = pages[0];
|
||||||
|
|
||||||
|
|||||||
@@ -637,11 +637,23 @@ export function usePosRoles() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useStaffShifts(tenantid: number | undefined, locationid: number | undefined) {
|
/**
|
||||||
|
* The shift windows this tenant's staff can be put on.
|
||||||
|
*
|
||||||
|
* Tenant-scoped, and the branch is optional. A shift is a fact about how a
|
||||||
|
* business runs — a tenant working 07:00–15:00 works those hours at every shop
|
||||||
|
* it owns — so requiring an outlet meant re-entering the same two windows per
|
||||||
|
* branch, which is how the third branch quietly ends up on 07:00–15:30.
|
||||||
|
*
|
||||||
|
* Passing a branch narrows the list to the tenant's shifts plus that branch's
|
||||||
|
* own, for the shops that genuinely differ. Omitting it returns everything the
|
||||||
|
* tenant has, which is what the pickers want.
|
||||||
|
*/
|
||||||
|
export function useStaffShifts(tenantid: number | undefined, locationid?: number) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: queryKeys.people.shifts(tenantid ?? 0, locationid ?? 0),
|
queryKey: queryKeys.people.shifts(tenantid ?? 0, locationid ?? 0),
|
||||||
queryFn: () => posUsersApi.shifts(tenantid as number, locationid as number),
|
queryFn: () => posUsersApi.shifts(tenantid as number, locationid),
|
||||||
enabled: Boolean(tenantid) && Boolean(locationid),
|
enabled: Boolean(tenantid),
|
||||||
...stable,
|
...stable,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user