Initial role user app
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import "server-only";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { clearSessionToken, getSessionToken } from "@/lib/session";
|
||||
import type { ApiEnvelope } from "@/lib/types";
|
||||
|
||||
export class BackendError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBackendBaseUrl() {
|
||||
return process.env.ACCESS_MANAGE_API_BASE_URL || "http://localhost:3500/api";
|
||||
}
|
||||
|
||||
function toUrl(path: string) {
|
||||
return new URL(path.replace(/^\//, ""), `${getBackendBaseUrl().replace(/\/$/, "")}/`);
|
||||
}
|
||||
|
||||
async function parseBackendResponse<T>(response: Response): Promise<T> {
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = contentType.includes("application/json") ? await response.json() : null;
|
||||
|
||||
if (!response.ok || payload?.success === false) {
|
||||
throw new BackendError(payload?.message || response.statusText || "请求失败", response.status);
|
||||
}
|
||||
|
||||
if (payload && typeof payload === "object" && "data" in payload) {
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
export async function backendRequest<T>(
|
||||
path: string,
|
||||
init: RequestInit & { token?: string; allowUnauthorized?: boolean } = {}
|
||||
) {
|
||||
const token = init.token ?? (await getSessionToken());
|
||||
const headers = new Headers(init.headers);
|
||||
|
||||
headers.set("Accept", "application/json");
|
||||
if (init.body && !headers.has("Content-Type")) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const response = await fetch(toUrl(path), {
|
||||
...init,
|
||||
headers,
|
||||
cache: "no-store"
|
||||
});
|
||||
|
||||
return parseBackendResponse<T>(response);
|
||||
}
|
||||
|
||||
export async function requireBackendData<T>(path: string) {
|
||||
try {
|
||||
return await backendRequest<T>(path);
|
||||
} catch (error) {
|
||||
if (error instanceof BackendError && error.status === 401) {
|
||||
redirect("/api/auth/logout?next=/login");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function proxyBackendJson<T>(path: string, init: RequestInit = {}) {
|
||||
try {
|
||||
const data = await backendRequest<T>(path, init);
|
||||
return Response.json({ success: true, data } satisfies ApiEnvelope<T>);
|
||||
} catch (error) {
|
||||
return backendErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function backendErrorResponse(error: unknown, fallbackMessage = "请求失败") {
|
||||
const status = error instanceof BackendError ? error.status : 500;
|
||||
const message = error instanceof Error ? error.message : fallbackMessage;
|
||||
|
||||
if (status === 401) {
|
||||
await clearSessionToken();
|
||||
}
|
||||
|
||||
return Response.json({ success: false, data: null, message }, { status });
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export function formatDateTime(value?: string | null) {
|
||||
if (!value) return "暂未设置";
|
||||
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
export function roleNames(roles: { name: string }[]) {
|
||||
return roles.map((role) => role.name).join("、") || "未分配角色";
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
import "server-only";
|
||||
|
||||
import { BackendError, backendRequest, requireBackendData } from "@/lib/backend";
|
||||
import type {
|
||||
AnnouncementDetail,
|
||||
AnnouncementSummary,
|
||||
AuthUser,
|
||||
MobileBootstrap,
|
||||
PermissionPayload,
|
||||
ShiftSummary,
|
||||
StoreDetail,
|
||||
StoreEmployee,
|
||||
TaskAssignee,
|
||||
TaskDetail,
|
||||
TaskEvent,
|
||||
TaskSummary
|
||||
} from "@/lib/types";
|
||||
|
||||
const EMPTY_TASKS: TaskSummary[] = [];
|
||||
const EMPTY_ANNOUNCEMENTS: AnnouncementSummary[] = [];
|
||||
const EMPTY_SHIFTS: ShiftSummary[] = [];
|
||||
const OPTIONAL_MOBILE_FALLBACK_STATUSES = [404, 500, 501, 502, 503];
|
||||
const OPTIONAL_STORE_FALLBACK_STATUSES = [403, 404, 500, 501, 502, 503];
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
export type TaskListFilter = {
|
||||
status?: TaskSummary["status"];
|
||||
};
|
||||
|
||||
export type ShiftListFilter = {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is UnknownRecord {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function stringOrUndefined(value: unknown) {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function numberOrUndefined(value: unknown) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function numericIdOrUndefined(value: unknown) {
|
||||
if (typeof value === "number" && Number.isInteger(value)) return value;
|
||||
if (typeof value === "string" && /^\d+$/.test(value)) return Number(value);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toList<T>(value: unknown, normalize: (item: unknown) => T | null): T[] {
|
||||
const source = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.items) ? value.items : [];
|
||||
return source.map(normalize).filter((item): item is T => item !== null);
|
||||
}
|
||||
|
||||
function normalizeTaskStatus(value: unknown): TaskSummary["status"] {
|
||||
switch (value) {
|
||||
case "PENDING":
|
||||
case "pending":
|
||||
return "pending";
|
||||
case "IN_PROGRESS":
|
||||
case "in_progress":
|
||||
return "in_progress";
|
||||
case "COMPLETED":
|
||||
case "completed":
|
||||
return "completed";
|
||||
case "CANCELLED":
|
||||
case "cancelled":
|
||||
return "cancelled";
|
||||
default:
|
||||
return "pending";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTaskPriority(value: unknown): TaskSummary["priority"] {
|
||||
switch (value) {
|
||||
case "LOW":
|
||||
case "low":
|
||||
return "low";
|
||||
case "HIGH":
|
||||
case "high":
|
||||
return "high";
|
||||
case "URGENT":
|
||||
case "urgent":
|
||||
return "urgent";
|
||||
default:
|
||||
return "normal";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAnnouncementLevel(value: unknown): AnnouncementSummary["level"] {
|
||||
switch (value) {
|
||||
case "IMPORTANT":
|
||||
case "important":
|
||||
return "important";
|
||||
case "URGENT":
|
||||
case "urgent":
|
||||
return "urgent";
|
||||
default:
|
||||
return "normal";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeShiftStatus(value: unknown): ShiftSummary["status"] {
|
||||
switch (value) {
|
||||
case "CANCELLED":
|
||||
case "cancelled":
|
||||
return "cancelled";
|
||||
case "COMPLETED":
|
||||
case "completed":
|
||||
return "completed";
|
||||
default:
|
||||
return "scheduled";
|
||||
}
|
||||
}
|
||||
|
||||
function summaryFromContent(value: unknown) {
|
||||
const content = stringOrUndefined(value);
|
||||
if (!content) return undefined;
|
||||
|
||||
const text = content.replace(/\s+/g, " ").trim();
|
||||
return text.length > 48 ? `${text.slice(0, 48)}...` : text;
|
||||
}
|
||||
|
||||
function normalizeTask(value: unknown): TaskSummary | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const title = stringOrUndefined(value.title) ?? "未命名任务";
|
||||
const assignees = toList(value.assignees, normalizeTaskAssignee);
|
||||
|
||||
return {
|
||||
id: String(value.id ?? title),
|
||||
title,
|
||||
description: stringOrUndefined(value.description),
|
||||
status: normalizeTaskStatus(value.status),
|
||||
priority: normalizeTaskPriority(value.priority),
|
||||
dueAt: stringOrUndefined(value.dueAt) ?? null,
|
||||
assigneeName: stringOrUndefined(value.assigneeName) ?? assignees[0]?.name,
|
||||
storeName: stringOrUndefined(value.storeName)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTaskAssignee(value: unknown): TaskAssignee | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const id = numericIdOrUndefined(value.id);
|
||||
const name = stringOrUndefined(value.name) ?? stringOrUndefined(value.displayName);
|
||||
if (id === undefined || !name) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
phone: stringOrUndefined(value.phone) ?? stringOrUndefined(value.username)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTaskEvent(value: unknown): TaskEvent | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const id = numericIdOrUndefined(value.id);
|
||||
const eventType = stringOrUndefined(value.eventType);
|
||||
if (id === undefined || !eventType) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
taskId: numericIdOrUndefined(value.taskId),
|
||||
eventType,
|
||||
fromStatus: stringOrUndefined(value.fromStatus) ?? null,
|
||||
toStatus: stringOrUndefined(value.toStatus) ?? null,
|
||||
comment: stringOrUndefined(value.comment) ?? null,
|
||||
createdAt: stringOrUndefined(value.createdAt) ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTaskDetail(value: unknown): TaskDetail {
|
||||
if (!isRecord(value)) {
|
||||
throw new BackendError("任务详情数据格式不正确", 502);
|
||||
}
|
||||
|
||||
const summary = normalizeTask(value);
|
||||
if (!summary) {
|
||||
throw new BackendError("任务详情数据格式不正确", 502);
|
||||
}
|
||||
|
||||
return {
|
||||
...summary,
|
||||
assignees: toList(value.assignees, normalizeTaskAssignee),
|
||||
events: toList(value.events, normalizeTaskEvent),
|
||||
createdAt: stringOrUndefined(value.createdAt) ?? null,
|
||||
updatedAt: stringOrUndefined(value.updatedAt) ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAnnouncement(value: unknown): AnnouncementSummary | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const title = stringOrUndefined(value.title) ?? "未命名公告";
|
||||
|
||||
return {
|
||||
id: String(value.id ?? title),
|
||||
title,
|
||||
summary: stringOrUndefined(value.summary) ?? summaryFromContent(value.content),
|
||||
level: normalizeAnnouncementLevel(value.level),
|
||||
publishedAt: stringOrUndefined(value.publishedAt) ?? null,
|
||||
read: typeof value.read === "boolean" ? value.read : Boolean(value.readAt)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAnnouncementDetail(value: unknown): AnnouncementDetail {
|
||||
if (!isRecord(value)) {
|
||||
throw new BackendError("公告详情数据格式不正确", 502);
|
||||
}
|
||||
|
||||
const summary = normalizeAnnouncement(value);
|
||||
if (!summary) {
|
||||
throw new BackendError("公告详情数据格式不正确", 502);
|
||||
}
|
||||
|
||||
return {
|
||||
...summary,
|
||||
content: stringOrUndefined(value.content) ?? summary.summary ?? "",
|
||||
readAt: stringOrUndefined(value.readAt) ?? null,
|
||||
createdAt: stringOrUndefined(value.createdAt) ?? null,
|
||||
updatedAt: stringOrUndefined(value.updatedAt) ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeShift(value: unknown): ShiftSummary | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const startAt = stringOrUndefined(value.startAt) ?? "";
|
||||
const endAt = stringOrUndefined(value.endAt) ?? "";
|
||||
|
||||
return {
|
||||
id: String(value.id ?? startAt),
|
||||
date: stringOrUndefined(value.date) ?? startAt.slice(0, 10),
|
||||
position: stringOrUndefined(value.position) ?? stringOrUndefined(value.roleName) ?? "班次",
|
||||
startAt,
|
||||
endAt,
|
||||
status: normalizeShiftStatus(value.status)
|
||||
};
|
||||
}
|
||||
|
||||
function firstShift(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return normalizeShift(value[0]);
|
||||
}
|
||||
|
||||
return normalizeShift(value);
|
||||
}
|
||||
|
||||
function shiftList(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(normalizeShift).filter((item): item is ShiftSummary => item !== null);
|
||||
}
|
||||
|
||||
const shift = normalizeShift(value);
|
||||
return shift ? [shift] : [];
|
||||
}
|
||||
|
||||
function permissionsFromBootstrap(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((item): item is string => typeof item === "string");
|
||||
}
|
||||
|
||||
if (isRecord(value)) {
|
||||
const codes = Array.isArray(value.codes) ? value.codes : value.permissions;
|
||||
return Array.isArray(codes) ? codes.filter((item): item is string => typeof item === "string") : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeRole(value: unknown) {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const id = numericIdOrUndefined(value.id);
|
||||
const code = stringOrUndefined(value.code);
|
||||
const name = stringOrUndefined(value.name);
|
||||
|
||||
if (id === undefined || !code || !name) return null;
|
||||
|
||||
return { id, code, name };
|
||||
}
|
||||
|
||||
function normalizeStoreEmployee(value: unknown): StoreEmployee | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const id = numericIdOrUndefined(value.id);
|
||||
const name = stringOrUndefined(value.name) ?? stringOrUndefined(value.displayName);
|
||||
if (id === undefined || !name) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
phone: stringOrUndefined(value.phone) ?? stringOrUndefined(value.username),
|
||||
status: stringOrUndefined(value.status),
|
||||
roles: toList(value.roles, normalizeRole)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStoreDetail(value: unknown, fallbackUser?: AuthUser): StoreDetail | null {
|
||||
if (!isRecord(value)) {
|
||||
if (!fallbackUser?.storeId) return null;
|
||||
|
||||
return {
|
||||
id: fallbackUser.storeId,
|
||||
name: fallbackUser.storeName ?? "当前门店",
|
||||
address: null,
|
||||
phone: null,
|
||||
status: undefined,
|
||||
employees: []
|
||||
};
|
||||
}
|
||||
|
||||
const id = numericIdOrUndefined(value.id) ?? fallbackUser?.storeId;
|
||||
const name = stringOrUndefined(value.name) ?? fallbackUser?.storeName;
|
||||
if (id === undefined || !name) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
address: stringOrUndefined(value.address) ?? null,
|
||||
phone: stringOrUndefined(value.phone) ?? null,
|
||||
status: stringOrUndefined(value.status),
|
||||
employees: toList(value.employees, normalizeStoreEmployee)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBootstrap(data: unknown): MobileBootstrap {
|
||||
if (!isRecord(data) || !isRecord(data.user)) {
|
||||
throw new BackendError("员工端首屏数据格式不正确", 502);
|
||||
}
|
||||
|
||||
const counters = isRecord(data.counters) ? data.counters : {};
|
||||
const tasks = toList(data.tasks, normalizeTask).slice(0, 5);
|
||||
const latestAnnouncements = toList(data.latestAnnouncements, normalizeAnnouncement).slice(0, 3);
|
||||
const todayShifts = shiftList(data.todayShifts ?? data.todayShift);
|
||||
const todayShift = firstShift(data.todayShift) ?? todayShifts[0] ?? null;
|
||||
const pendingTasks = tasks.filter((task) => task.status === "pending" || task.status === "in_progress");
|
||||
const now = Date.now();
|
||||
|
||||
return {
|
||||
user: data.user as AuthUser,
|
||||
permissions: permissionsFromBootstrap(data.permissions),
|
||||
unreadAnnouncementCount:
|
||||
numberOrUndefined(data.unreadAnnouncementCount ?? counters.unreadAnnouncementCount) ??
|
||||
latestAnnouncements.filter((item) => !item.read).length,
|
||||
pendingTaskCount: numberOrUndefined(data.pendingTaskCount ?? counters.pendingTaskCount) ?? pendingTasks.length,
|
||||
overdueTaskCount:
|
||||
numberOrUndefined(data.overdueTaskCount ?? counters.overdueTaskCount) ??
|
||||
pendingTasks.filter((task) => task.dueAt && new Date(task.dueAt).getTime() < now).length,
|
||||
todayShift,
|
||||
todayShifts,
|
||||
latestAnnouncements,
|
||||
tasks
|
||||
};
|
||||
}
|
||||
|
||||
async function optionalBackendData<T>(path: string, fallback: T, fallbackStatuses = [404]) {
|
||||
try {
|
||||
return await backendRequest<T>(path);
|
||||
} catch (error) {
|
||||
if (error instanceof BackendError && fallbackStatuses.includes(error.status)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function withSearch(path: string, entries: Record<string, string | undefined>) {
|
||||
const search = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
if (value) {
|
||||
search.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const query = search.toString();
|
||||
return query ? `${path}?${query}` : path;
|
||||
}
|
||||
|
||||
function taskStatusToBackend(status?: TaskSummary["status"]) {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "PENDING";
|
||||
case "in_progress":
|
||||
return "IN_PROGRESS";
|
||||
case "completed":
|
||||
return "COMPLETED";
|
||||
case "cancelled":
|
||||
return "CANCELLED";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBootstrapData(): Promise<MobileBootstrap> {
|
||||
try {
|
||||
return normalizeBootstrap(await requireBackendData<unknown>("/mobile/bootstrap"));
|
||||
} catch (error) {
|
||||
if (!(error instanceof BackendError && OPTIONAL_MOBILE_FALLBACK_STATUSES.includes(error.status))) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const [user, permissionPayload, tasks, announcements, todayShift] = await Promise.all([
|
||||
requireBackendData<AuthUser>("/auth/me"),
|
||||
requireBackendData<PermissionPayload>("/permissions/me"),
|
||||
optionalBackendData<unknown>("/mobile/tasks", EMPTY_TASKS, OPTIONAL_MOBILE_FALLBACK_STATUSES),
|
||||
optionalBackendData<unknown>("/mobile/announcements", EMPTY_ANNOUNCEMENTS, OPTIONAL_MOBILE_FALLBACK_STATUSES),
|
||||
optionalBackendData<unknown>("/mobile/shifts/today", null, OPTIONAL_MOBILE_FALLBACK_STATUSES)
|
||||
]);
|
||||
|
||||
const now = Date.now();
|
||||
const normalizedTasks = toList(tasks, normalizeTask);
|
||||
const normalizedAnnouncements = toList(announcements, normalizeAnnouncement);
|
||||
const todayShifts = shiftList(todayShift);
|
||||
const pendingTasks = normalizedTasks.filter((task) => task.status === "pending" || task.status === "in_progress");
|
||||
|
||||
return {
|
||||
user,
|
||||
permissions: permissionPayload.permissions,
|
||||
unreadAnnouncementCount: normalizedAnnouncements.filter((item) => !item.read).length,
|
||||
pendingTaskCount: pendingTasks.length,
|
||||
overdueTaskCount: pendingTasks.filter((task) => task.dueAt && new Date(task.dueAt).getTime() < now).length,
|
||||
todayShift: todayShifts[0] ?? null,
|
||||
todayShifts,
|
||||
latestAnnouncements: normalizedAnnouncements.slice(0, 3),
|
||||
tasks: normalizedTasks.slice(0, 5)
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCurrentUser() {
|
||||
return requireBackendData<AuthUser>("/auth/me");
|
||||
}
|
||||
|
||||
export async function getPermissionPayload() {
|
||||
return requireBackendData<PermissionPayload>("/permissions/me");
|
||||
}
|
||||
|
||||
export async function getTasks(filter: TaskListFilter = {}) {
|
||||
const path = withSearch("/mobile/tasks", {
|
||||
status: taskStatusToBackend(filter.status)
|
||||
});
|
||||
|
||||
return toList(
|
||||
await optionalBackendData<unknown>(path, EMPTY_TASKS, OPTIONAL_MOBILE_FALLBACK_STATUSES),
|
||||
normalizeTask
|
||||
);
|
||||
}
|
||||
|
||||
export async function getTaskDetail(id: string) {
|
||||
return normalizeTaskDetail(await requireBackendData<unknown>(`/mobile/tasks/${encodeURIComponent(id)}`));
|
||||
}
|
||||
|
||||
export async function getAnnouncements() {
|
||||
return toList(
|
||||
await optionalBackendData<unknown>("/mobile/announcements", EMPTY_ANNOUNCEMENTS, OPTIONAL_MOBILE_FALLBACK_STATUSES),
|
||||
normalizeAnnouncement
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAnnouncementDetail(id: string) {
|
||||
return normalizeAnnouncementDetail(
|
||||
await requireBackendData<unknown>(`/mobile/announcements/${encodeURIComponent(id)}`)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getShifts(filter: ShiftListFilter = {}) {
|
||||
const path = withSearch("/mobile/shifts", {
|
||||
startDate: filter.startDate,
|
||||
endDate: filter.endDate
|
||||
});
|
||||
|
||||
return toList(
|
||||
await optionalBackendData<unknown>(path, EMPTY_SHIFTS, OPTIONAL_MOBILE_FALLBACK_STATUSES),
|
||||
normalizeShift
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCurrentStore(user?: AuthUser) {
|
||||
const currentUser = user ?? (await getCurrentUser());
|
||||
if (!currentUser.storeId) return null;
|
||||
|
||||
const fallback = normalizeStoreDetail(null, currentUser);
|
||||
return normalizeStoreDetail(
|
||||
await optionalBackendData<unknown>(`/stores/${currentUser.storeId}`, fallback, OPTIONAL_STORE_FALLBACK_STATUSES),
|
||||
currentUser
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCurrentStoreEmployees(user?: AuthUser) {
|
||||
const currentUser = user ?? (await getCurrentUser());
|
||||
if (!currentUser.storeId) return [];
|
||||
|
||||
const search = new URLSearchParams({
|
||||
storeId: String(currentUser.storeId),
|
||||
page: "1",
|
||||
pageSize: "100"
|
||||
});
|
||||
|
||||
return toList(
|
||||
await optionalBackendData<unknown>(`/employees?${search.toString()}`, [], OPTIONAL_STORE_FALLBACK_STATUSES),
|
||||
normalizeStoreEmployee
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import "server-only";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const DEFAULT_COOKIE_NAME = "role_user_session";
|
||||
|
||||
export function getSessionCookieName() {
|
||||
return process.env.ROLE_USER_SESSION_COOKIE || DEFAULT_COOKIE_NAME;
|
||||
}
|
||||
|
||||
export async function getSessionToken() {
|
||||
return (await cookies()).get(getSessionCookieName())?.value;
|
||||
}
|
||||
|
||||
export async function setSessionToken(token: string) {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
cookieStore.set(getSessionCookieName(), token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 8
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearSessionToken() {
|
||||
(await cookies()).delete(getSessionCookieName());
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
export type AccountType = "SUPER_ADMIN" | "EMPLOYEE";
|
||||
|
||||
export type Role = {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AuthUser = {
|
||||
id: number;
|
||||
username: string;
|
||||
displayName: string;
|
||||
accountType: AccountType;
|
||||
storeId?: number;
|
||||
storeName?: string;
|
||||
roles: Role[];
|
||||
permissions: string[];
|
||||
canManage: boolean;
|
||||
lastLoginAt?: string | null;
|
||||
mustChangePassword?: boolean;
|
||||
};
|
||||
|
||||
export type PermissionMenu = {
|
||||
key: string;
|
||||
title: string;
|
||||
permission: string;
|
||||
actions: string[];
|
||||
};
|
||||
|
||||
export type PermissionPayload = {
|
||||
permissions: string[];
|
||||
menus: PermissionMenu[];
|
||||
};
|
||||
|
||||
export type ApiEnvelope<T> = {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type LoginResponse = {
|
||||
token: string;
|
||||
tokenType: "Bearer";
|
||||
expiresIn: string;
|
||||
user: AuthUser;
|
||||
};
|
||||
|
||||
export type TaskStatus = "pending" | "in_progress" | "completed" | "cancelled";
|
||||
|
||||
export type TaskSummary = {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: TaskStatus;
|
||||
priority: "low" | "normal" | "high" | "urgent";
|
||||
dueAt?: string | null;
|
||||
assigneeName?: string;
|
||||
storeName?: string;
|
||||
};
|
||||
|
||||
export type TaskAssignee = {
|
||||
id: number;
|
||||
name: string;
|
||||
phone?: string;
|
||||
};
|
||||
|
||||
export type TaskEvent = {
|
||||
id: number;
|
||||
taskId?: number;
|
||||
eventType: "CREATED" | "UPDATED" | "STARTED" | "COMPLETED" | "CANCELLED" | "COMMENTED" | string;
|
||||
fromStatus?: string | null;
|
||||
toStatus?: string | null;
|
||||
comment?: string | null;
|
||||
createdAt?: string | null;
|
||||
};
|
||||
|
||||
export type TaskDetail = TaskSummary & {
|
||||
assignees: TaskAssignee[];
|
||||
events: TaskEvent[];
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
export type AnnouncementSummary = {
|
||||
id: string;
|
||||
title: string;
|
||||
summary?: string;
|
||||
level: "normal" | "important" | "urgent";
|
||||
publishedAt?: string | null;
|
||||
read: boolean;
|
||||
};
|
||||
|
||||
export type AnnouncementDetail = AnnouncementSummary & {
|
||||
content: string;
|
||||
readAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
export type ShiftSummary = {
|
||||
id: string;
|
||||
date: string;
|
||||
position: string;
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
status: "scheduled" | "cancelled" | "completed";
|
||||
};
|
||||
|
||||
export type StoreEmployee = {
|
||||
id: number;
|
||||
name: string;
|
||||
phone?: string;
|
||||
status?: string;
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
export type StoreDetail = {
|
||||
id: number;
|
||||
name: string;
|
||||
address?: string | null;
|
||||
phone?: string | null;
|
||||
status?: string;
|
||||
employees: StoreEmployee[];
|
||||
};
|
||||
|
||||
export type MobileBootstrap = {
|
||||
user: AuthUser;
|
||||
permissions: string[];
|
||||
unreadAnnouncementCount: number;
|
||||
pendingTaskCount: number;
|
||||
overdueTaskCount: number;
|
||||
todayShift: ShiftSummary | null;
|
||||
todayShifts: ShiftSummary[];
|
||||
latestAnnouncements: AnnouncementSummary[];
|
||||
tasks: TaskSummary[];
|
||||
};
|
||||
Reference in New Issue
Block a user