shifts updated for pos
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
AlertTriangle,
|
||||
Check,
|
||||
Copy,
|
||||
Clock,
|
||||
KeyRound,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
POS_ROLE_CASHIER,
|
||||
generatePosPassword,
|
||||
type PosRoleOption,
|
||||
type PosStaffShift,
|
||||
type PosStaffUser,
|
||||
} from '../services/fiestaApi';
|
||||
import {
|
||||
@@ -43,7 +45,10 @@ import {
|
||||
useDeactivatePosStaff,
|
||||
usePosRoles,
|
||||
usePosStaff,
|
||||
useCreateStaffShift,
|
||||
useStaffShifts,
|
||||
useUpdatePosStaff,
|
||||
useUpdateStaffShift,
|
||||
} from '../services/fiestaQueries';
|
||||
import {
|
||||
BORDER,
|
||||
@@ -89,6 +94,7 @@ export default function PosStaffPanel({
|
||||
}: PosStaffPanelProps) {
|
||||
const staffQ = usePosStaff(tenantId, locationid);
|
||||
const rolesQ = usePosRoles();
|
||||
const shiftsQ = useStaffShifts(tenantId, locationid);
|
||||
const createMut = useCreatePosStaff();
|
||||
const updateMut = useUpdatePosStaff();
|
||||
const deactivateMut = useDeactivatePosStaff();
|
||||
@@ -97,6 +103,8 @@ export default function PosStaffPanel({
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [role, setRole] = useState('');
|
||||
const [pin, setPin] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [shiftId, setShiftId] = useState(0);
|
||||
const [formError, setFormError] = useState('');
|
||||
const [issued, setIssued] = useState<PosStaffUser | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -109,6 +117,8 @@ export default function PosStaffPanel({
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editRole, setEditRole] = useState('');
|
||||
const [editPin, setEditPin] = useState('');
|
||||
const [editPhone, setEditPhone] = useState('');
|
||||
const [editShift, setEditShift] = useState(0);
|
||||
const [editError, setEditError] = useState('');
|
||||
const [confirmReset, setConfirmReset] = useState<PosStaffUser | null>(null);
|
||||
const [rowError, setRowError] = useState('');
|
||||
@@ -125,6 +135,8 @@ export default function PosStaffPanel({
|
||||
setFullName('');
|
||||
setRole('');
|
||||
setPin('');
|
||||
setPhone('');
|
||||
setShiftId(0);
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
@@ -140,10 +152,26 @@ export default function PosStaffPanel({
|
||||
if (staff.some((s) => s.pin === pin && (s.status || '').toLowerCase() !== 'inactive')) {
|
||||
return setFormError('Another active person at this outlet already uses that PIN.');
|
||||
}
|
||||
// The till signs in with this, so it is required here even though the
|
||||
// column allows blank — an account without one can only ever log in by
|
||||
// username, which is what this change exists to stop relying on.
|
||||
const digits = phone.replace(/\D/g, '').replace(/^91(?=\d{10}$)/, '').replace(/^0(?=\d{10}$)/, '');
|
||||
if (digits.length !== 10) return setFormError('Enter a 10-digit mobile number.');
|
||||
if (staff.some((s) => (s.contactno || '') === digits && (s.status || '').toLowerCase() !== 'inactive')) {
|
||||
return setFormError('Another active person here already signs in with that number.');
|
||||
}
|
||||
|
||||
setFormError('');
|
||||
createMut.mutate(
|
||||
{ tenantid: tenantId, locationid, full_name: name, role: chosen, pin },
|
||||
{
|
||||
tenantid: tenantId,
|
||||
locationid,
|
||||
full_name: name,
|
||||
role: chosen,
|
||||
pin,
|
||||
contactno: digits,
|
||||
shift_id: shiftId || undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: (created) => {
|
||||
setIssued(created);
|
||||
@@ -161,6 +189,8 @@ export default function PosStaffPanel({
|
||||
setEditName(s.full_name || '');
|
||||
setEditRole(s.role_id === POS_ROLE_CASHIER ? 'cashier' : 'supervisor');
|
||||
setEditPin(s.pin || '');
|
||||
setEditPhone(s.contactno || '');
|
||||
setEditShift(s.shift_id || 0);
|
||||
setEditError('');
|
||||
};
|
||||
|
||||
@@ -180,6 +210,8 @@ export default function PosStaffPanel({
|
||||
// role as a change, and a no-op write still moves the row.
|
||||
role: editRole && editRole !== (editing.role || '').toLowerCase() ? editRole : undefined,
|
||||
pin: editPin && editPin !== editing.pin ? editPin : undefined,
|
||||
contactno: editPhone && editPhone !== (editing.contactno || '') ? editPhone : undefined,
|
||||
shift_id: editShift && editShift !== (editing.shift_id || 0) ? editShift : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => setEditing(null),
|
||||
@@ -241,6 +273,13 @@ export default function PosStaffPanel({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShiftManager
|
||||
tenantId={tenantId}
|
||||
locationid={locationid}
|
||||
shifts={shiftsQ.data ?? []}
|
||||
loading={shiftsQ.isLoading}
|
||||
/>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-start gap-2">
|
||||
@@ -293,8 +332,35 @@ export default function PosStaffPanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
|
||||
<input
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value.replace(/[^\d+\s-]/g, '').slice(0, 16))}
|
||||
placeholder="Mobile number — used to sign in"
|
||||
inputMode="tel"
|
||||
className="rounded-lg px-3 py-2 text-[13px] outline-none bg-white font-mono"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
/>
|
||||
<select
|
||||
value={shiftId}
|
||||
onChange={(e) => setShiftId(Number(e.target.value))}
|
||||
className="rounded-lg px-3 py-2 text-[13px] outline-none bg-white cursor-pointer"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
>
|
||||
<option value={0}>
|
||||
{shiftsQ.data?.length ? 'No shift assigned' : 'No shifts set up yet'}
|
||||
</option>
|
||||
{(shiftsQ.data ?? []).map((sh) => (
|
||||
<option key={sh.staff_shift_id} value={sh.staff_shift_id}>
|
||||
{sh.name} · {sh.start_time}–{sh.end_time}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] mt-2" style={{ color: TEXT_3 }}>
|
||||
A username and password are generated. The password is shown once, on the next screen.
|
||||
The mobile number is what they type at the till. A username and password are also
|
||||
generated; the password is shown once, on the next screen.
|
||||
</p>
|
||||
|
||||
{formError && (
|
||||
@@ -345,11 +411,13 @@ export default function PosStaffPanel({
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl border overflow-hidden shadow-sm w-full min-w-0" style={{ borderColor: BORDER }}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm whitespace-nowrap" style={{ minWidth: 720 }}>
|
||||
<table className="w-full text-sm whitespace-nowrap" style={{ minWidth: 940 }}>
|
||||
<thead style={TH_STYLE}>
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left">Name</th>
|
||||
<th className="px-4 py-3 text-left">Role</th>
|
||||
<th className="px-4 py-3 text-left">Mobile</th>
|
||||
<th className="px-4 py-3 text-left">Shift</th>
|
||||
<th className="px-4 py-3 text-left">Username</th>
|
||||
<th className="px-4 py-3 text-left">PIN</th>
|
||||
<th className="px-4 py-3 text-left">Password</th>
|
||||
@@ -369,6 +437,12 @@ export default function PosStaffPanel({
|
||||
color={s.role_id === POS_ROLE_CASHIER ? BRAND : '#0ea5e9'}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-[12px]" style={{ color: s.contactno ? TEXT : '#f59e0b' }}>
|
||||
{s.contactno || 'not set'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[12px]" style={{ color: TEXT_2 }}>
|
||||
{s.shift_name ? `${s.shift_name} · ${s.shift_start}–${s.shift_end}` : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-[12px]" style={{ color: TEXT_2 }}>{s.authname || '—'}</td>
|
||||
<td className="px-4 py-3 font-mono tracking-widest" style={{ color: TEXT_2 }}>{s.pin || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -455,6 +529,27 @@ export default function PosStaffPanel({
|
||||
className="w-full rounded-lg px-3 py-2 text-[13px] outline-none font-mono tracking-widest"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
/>
|
||||
<input
|
||||
value={editPhone}
|
||||
onChange={(e) => setEditPhone(e.target.value.replace(/[^\d+\s-]/g, '').slice(0, 16))}
|
||||
placeholder="Mobile number — used to sign in"
|
||||
inputMode="tel"
|
||||
className="w-full rounded-lg px-3 py-2 text-[13px] outline-none font-mono"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
/>
|
||||
<select
|
||||
value={editShift}
|
||||
onChange={(e) => setEditShift(Number(e.target.value))}
|
||||
className="w-full rounded-lg px-3 py-2 text-[13px] outline-none cursor-pointer"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
>
|
||||
<option value={0}>No shift assigned</option>
|
||||
{(shiftsQ.data ?? []).map((sh) => (
|
||||
<option key={sh.staff_shift_id} value={sh.staff_shift_id}>
|
||||
{sh.name} · {sh.start_time}–{sh.end_time}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{editError && <p className="text-[12px] mt-2 font-semibold" style={{ color: '#ef4444' }}>{editError}</p>}
|
||||
<div className="flex flex-wrap items-center gap-2 mt-4">
|
||||
@@ -519,6 +614,7 @@ export default function PosStaffPanel({
|
||||
<div className="rounded-xl p-3 space-y-2" style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}` }}>
|
||||
<Field label="Name" value={issued.full_name} />
|
||||
<Field label="Role" value={issued.role} />
|
||||
<Field label="Mobile (sign-in)" value={issued.contactno || '—'} mono />
|
||||
<Field label="Username" value={issued.authname || '—'} mono />
|
||||
<Field label="Password" value={issued.password || '—'} mono />
|
||||
<Field label="PIN" value={issued.pin || '—'} mono />
|
||||
@@ -589,6 +685,224 @@ export default function PosStaffPanel({
|
||||
);
|
||||
}
|
||||
|
||||
const WEEKDAY_LABELS = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
|
||||
|
||||
/**
|
||||
* The shift windows an outlet runs.
|
||||
*
|
||||
* Sits above the staff list because it has to: the shift picker on the staff
|
||||
* form can only offer what exists here, and without this screen that dropdown
|
||||
* reads "No shifts set up yet" for ever.
|
||||
*
|
||||
* Deliberately small. A shift is a name and two times — the moment it grows a
|
||||
* roster of dated assignments it wants its own page, but a shop that runs a
|
||||
* morning and an evening should not have to visit one.
|
||||
*/
|
||||
function ShiftManager({
|
||||
tenantId,
|
||||
locationid,
|
||||
shifts,
|
||||
loading,
|
||||
}: {
|
||||
tenantId: number;
|
||||
locationid: number;
|
||||
shifts: PosStaffShift[];
|
||||
loading: boolean;
|
||||
}) {
|
||||
const createMut = useCreateStaffShift();
|
||||
const updateMut = useUpdateStaffShift();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [start, setStart] = useState('');
|
||||
const [end, setEnd] = useState('');
|
||||
// All seven on by default: a shop that never varies its week should not have
|
||||
// to say so, and the server treats an all-ones mask and an empty one alike.
|
||||
const [days, setDays] = useState<boolean[]>(() => Array(7).fill(true));
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const toggleDay = (i: number) =>
|
||||
setDays((d) => d.map((on, idx) => (idx === i ? !on : on)));
|
||||
|
||||
const submit = () => {
|
||||
if (!name.trim()) return setError('Give the shift a name.');
|
||||
if (!start || !end) return setError('Set a start and an end time.');
|
||||
if (start === end) return setError('A shift cannot start and end at the same time.');
|
||||
if (!days.some(Boolean)) return setError('Pick at least one day.');
|
||||
|
||||
setError('');
|
||||
createMut.mutate(
|
||||
{
|
||||
tenantid: tenantId,
|
||||
locationid,
|
||||
name: name.trim(),
|
||||
start_time: start,
|
||||
end_time: end,
|
||||
weekdays: days.every(Boolean) ? '' : days.map((d) => (d ? '1' : '0')).join(''),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setName('');
|
||||
setStart('');
|
||||
setEnd('');
|
||||
setDays(Array(7).fill(true));
|
||||
},
|
||||
onError: (e: unknown) => setError((e as Error)?.message || 'Could not create the shift.'),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const active = shifts.filter((s) => (s.status || '').toLowerCase() !== 'inactive');
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-start gap-2">
|
||||
<Clock size={14} style={{ color: TEXT_2 }} className="mt-0.5" />
|
||||
<div>
|
||||
<h3 className="text-[13px] font-extrabold leading-tight" style={{ color: TEXT }}>
|
||||
Shifts
|
||||
</h3>
|
||||
<p className="text-[11px] mt-0.5" style={{ color: TEXT_3 }}>
|
||||
{loading
|
||||
? 'Loading…'
|
||||
: active.length
|
||||
? `${active.length} window${active.length === 1 ? '' : 's'} — assign one to a person below.`
|
||||
: 'None yet. Add one before you can assign it to a cashier.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Pill active={open} color={BRAND} onClick={() => { setOpen(!open); setError(''); }}>
|
||||
<Plus size={12} />
|
||||
Add shift
|
||||
</Pill>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div className="mt-4 rounded-xl p-3" style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}` }}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name — e.g. Morning"
|
||||
className="rounded-lg px-3 py-2 text-[13px] outline-none bg-white"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
/>
|
||||
<input
|
||||
type="time"
|
||||
value={start}
|
||||
onChange={(e) => setStart(e.target.value)}
|
||||
className="rounded-lg px-3 py-2 text-[13px] outline-none bg-white"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
/>
|
||||
<input
|
||||
type="time"
|
||||
value={end}
|
||||
onChange={(e) => setEnd(e.target.value)}
|
||||
className="rounded-lg px-3 py-2 text-[13px] outline-none bg-white"
|
||||
style={{ border: `1px solid ${BORDER}`, color: TEXT }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 mt-3 flex-wrap">
|
||||
<span className="text-[11px] font-extrabold uppercase tracking-wide mr-1" style={{ color: TEXT_3 }}>
|
||||
Days
|
||||
</span>
|
||||
{WEEKDAY_LABELS.map((d, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => toggleDay(i)}
|
||||
className="rounded-full text-[11px] font-extrabold cursor-pointer"
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
background: days[i] ? BRAND : '#fff',
|
||||
color: days[i] ? '#fff' : TEXT_3,
|
||||
border: `1px solid ${days[i] ? BRAND : BORDER}`,
|
||||
}}
|
||||
>
|
||||
{d}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] mt-2" style={{ color: TEXT_3 }}>
|
||||
An end before the start is fine — a night shift running 22:00 to 06:00 wraps midnight.
|
||||
</p>
|
||||
|
||||
{error && <p className="text-[12px] mt-2 font-semibold" style={{ color: '#ef4444' }}>{error}</p>}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mt-3">
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={createMut.isPending}
|
||||
className="rounded-full px-4 py-1.5 text-[12.5px] font-bold text-white cursor-pointer disabled:opacity-50"
|
||||
style={{ background: BRAND }}
|
||||
>
|
||||
{createMut.isPending ? 'Adding…' : 'Add shift'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setOpen(false); setError(''); }}
|
||||
className="rounded-full px-4 py-1.5 text-[12.5px] font-bold cursor-pointer"
|
||||
style={{ background: '#fff', color: TEXT_2, border: `1px solid ${BORDER}` }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shifts.length > 0 && (
|
||||
<div className="mt-3 divide-y" style={{ borderColor: DIVIDER }}>
|
||||
{shifts.map((s) => {
|
||||
const inactive = (s.status || '').toLowerCase() === 'inactive';
|
||||
return (
|
||||
<div
|
||||
key={s.staff_shift_id}
|
||||
className="flex items-center justify-between gap-3 py-2"
|
||||
style={{ opacity: inactive ? 0.5 : 1 }}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[12.5px] font-bold" style={{ color: TEXT }}>
|
||||
{s.name}
|
||||
<span className="font-mono font-normal ml-2" style={{ color: TEXT_2 }}>
|
||||
{s.start_time}–{s.end_time}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[11px]" style={{ color: TEXT_3 }}>
|
||||
{!s.weekdays || s.weekdays === '1111111'
|
||||
? 'Every day'
|
||||
: WEEKDAY_LABELS.filter((_, i) => s.weekdays?.[i] === '1').join(' ') || '—'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
updateMut.mutate({
|
||||
tenantid: tenantId,
|
||||
locationid,
|
||||
staff_shift_id: s.staff_shift_id,
|
||||
status: inactive ? 'Active' : 'Inactive',
|
||||
})
|
||||
}
|
||||
disabled={updateMut.isPending}
|
||||
className="rounded-full px-3 py-1 text-[11.5px] font-bold cursor-pointer shrink-0 disabled:opacity-50"
|
||||
style={
|
||||
inactive
|
||||
? { background: tint('#10b981'), color: '#10b981', border: `1px solid ${edge('#10b981')}` }
|
||||
: { background: '#fff', color: TEXT_2, border: `1px solid ${BORDER}` }
|
||||
}
|
||||
>
|
||||
{inactive ? 'Reactivate' : 'Retire'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3 text-[12.5px]">
|
||||
|
||||
@@ -1701,6 +1701,11 @@ export interface PosStaffUser {
|
||||
role: string;
|
||||
pin?: string;
|
||||
has_password: boolean;
|
||||
/** Resolved shift. Blank on accounts created before shifts existed. */
|
||||
shift_id?: number;
|
||||
shift_name?: string;
|
||||
shift_start?: string;
|
||||
shift_end?: string;
|
||||
/** Present ONLY in the answer to a create or a reset. Never in a listing. */
|
||||
password?: string;
|
||||
location_id: number;
|
||||
@@ -1758,9 +1763,69 @@ export interface CreatePosStaffInput {
|
||||
/** "supervisor" | "cashier" — from getPosRoles, not a hardcoded literal. */
|
||||
role: string;
|
||||
pin: string;
|
||||
/**
|
||||
* The mobile number this person signs in at the till with.
|
||||
*
|
||||
* Normalised to ten digits server-side and required to be unique among a
|
||||
* tenant's till accounts, because the terminal's sign-in matches on it.
|
||||
*/
|
||||
contactno?: string;
|
||||
/** staffshifts.staff_shift_id. Omit or 0 to leave unassigned. */
|
||||
shift_id?: number;
|
||||
/** Optional. Omitted, the server generates one and walks past collisions. */
|
||||
authname?: string;
|
||||
contactno?: string;
|
||||
}
|
||||
|
||||
/** A working window a till account can be assigned to. */
|
||||
export interface PosStaffShift {
|
||||
staff_shift_id: number;
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
name: string;
|
||||
/** 24-hour `HH:MM`. */
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
/** Seven 0/1 characters from Monday. Empty means every day. */
|
||||
weekdays?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** GET /tenants/getstaffshifts — the windows configured at one outlet. */
|
||||
export async function getStaffShifts(opts: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
}): Promise<PosStaffShift[]> {
|
||||
const json = await fiestaGet<{ details?: { shifts?: PosStaffShift[] } | PosStaffShift[] }>(
|
||||
'tenants/getstaffshifts',
|
||||
{ tenantid: opts.tenantid, locationid: opts.locationid },
|
||||
);
|
||||
const d = json?.details;
|
||||
if (Array.isArray(d)) return d;
|
||||
return d?.shifts ?? [];
|
||||
}
|
||||
|
||||
export async function createStaffShift(input: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
name: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
weekdays?: string;
|
||||
}): Promise<Row> {
|
||||
return fiestaSend<Row>('tenants/createstaffshift', 'POST', input);
|
||||
}
|
||||
|
||||
export async function updateStaffShift(input: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
staff_shift_id: number;
|
||||
name?: string;
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
weekdays?: string;
|
||||
status?: string;
|
||||
}): Promise<Row> {
|
||||
return fiestaSend<Row>('tenants/updatestaffshift', 'PUT', input);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1808,6 +1873,8 @@ export async function updatePosStaff(input: {
|
||||
pin?: string;
|
||||
authname?: string;
|
||||
contactno?: string;
|
||||
/** staffshifts.staff_shift_id. Zero or omitted leaves the shift unchanged. */
|
||||
shift_id?: number;
|
||||
password?: string;
|
||||
status?: string;
|
||||
}): Promise<Row> {
|
||||
|
||||
@@ -53,6 +53,9 @@ import {
|
||||
createPosStaff,
|
||||
updatePosStaff,
|
||||
deactivatePosStaff,
|
||||
getStaffShifts,
|
||||
createStaffShift,
|
||||
updateStaffShift,
|
||||
getMasterCatalog,
|
||||
getProductCategories,
|
||||
getProductSubcategories,
|
||||
@@ -1121,3 +1124,30 @@ export function useDeactivatePosStaff() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Working windows at one outlet, for the shift picker on the staff form. */
|
||||
export function useStaffShifts(tenantid: number, locationid: number) {
|
||||
return useQuery({
|
||||
queryKey: ['fiesta', 'staffshifts', tenantid, locationid],
|
||||
queryFn: () => getStaffShifts({ tenantid, locationid }),
|
||||
enabled: Boolean(tenantid) && Boolean(locationid),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateStaffShift() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createStaffShift,
|
||||
onSuccess: (_r, v) =>
|
||||
qc.invalidateQueries({ queryKey: ['fiesta', 'staffshifts', v.tenantid, v.locationid] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateStaffShift() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: updateStaffShift,
|
||||
onSuccess: (_r, v) =>
|
||||
qc.invalidateQueries({ queryKey: ['fiesta', 'staffshifts', v.tenantid, v.locationid] }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user