292 lines
9.0 KiB
TypeScript
292 lines
9.0 KiB
TypeScript
import React, { useState } from "react";
|
|
import { View, TextInput, StyleSheet, Pressable } from "react-native";
|
|
import { useSirouRouter } from "@sirou/react-native";
|
|
import { useColorScheme } from "nativewind";
|
|
import { api } from "@/lib/api";
|
|
import { AppRoutes } from "@/lib/routes";
|
|
import { toast } from "@/lib/toast-store";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { FormFlow } from "@/components/FormFlow";
|
|
import { Text } from "@/components/ui/text";
|
|
import { PickerModal, SelectOption } from "@/components/PickerModal";
|
|
import {
|
|
Mail,
|
|
Phone,
|
|
User,
|
|
Lock,
|
|
ShieldCheck,
|
|
ChevronDown,
|
|
} from "@/lib/icons";
|
|
|
|
const S = StyleSheet.create({
|
|
input: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 12,
|
|
fontSize: 12,
|
|
fontWeight: "500",
|
|
borderRadius: 6,
|
|
borderWidth: 1,
|
|
textAlignVertical: "center",
|
|
},
|
|
});
|
|
|
|
function useInputColors() {
|
|
const { colorScheme } = useColorScheme();
|
|
const dark = colorScheme === "dark";
|
|
return {
|
|
bg: dark ? "rgba(30,30,30,0.8)" : "rgba(241,245,249,0.2)",
|
|
border: dark ? "rgba(255,255,255,0.08)" : "rgba(203,213,225,0.6)",
|
|
text: dark ? "#f1f5f9" : "#0f172a",
|
|
placeholder: "rgba(100,116,139,0.45)",
|
|
};
|
|
}
|
|
|
|
function Field({
|
|
label,
|
|
value,
|
|
onChangeText,
|
|
placeholder,
|
|
icon,
|
|
flex,
|
|
numeric = false,
|
|
secureTextEntry = false,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChangeText: (v: string) => void;
|
|
placeholder: string;
|
|
icon?: React.ReactNode;
|
|
flex?: number;
|
|
numeric?: boolean;
|
|
secureTextEntry?: boolean;
|
|
}) {
|
|
const c = useInputColors();
|
|
return (
|
|
<View style={flex != null ? { flex } : undefined}>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
{label}
|
|
</Text>
|
|
<View style={[S.input, { backgroundColor: c.bg, borderColor: c.border, flexDirection: "row", alignItems: "center" }]}>
|
|
{icon}
|
|
<TextInput
|
|
style={{ flex: 1, marginLeft: icon ? 8 : 0, color: c.text, fontSize: 13, fontWeight: "500" }}
|
|
placeholder={placeholder}
|
|
placeholderTextColor={c.placeholder}
|
|
value={value}
|
|
onChangeText={onChangeText}
|
|
keyboardType={numeric ? "numeric" : "default"}
|
|
secureTextEntry={secureTextEntry}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const ROLES = ["VIEWER", "EMPLOYEE", "ACCOUNTANT", "CUSTOMER_SERVICE"];
|
|
|
|
const STEPS = [
|
|
{ key: "name", label: "Name" },
|
|
{ key: "contact", label: "Contact" },
|
|
{ key: "access", label: "Access" },
|
|
];
|
|
|
|
export default function CreateTeamMemberScreen() {
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
const { colorScheme } = useColorScheme();
|
|
const isDark = colorScheme === "dark";
|
|
const iconColor = isDark ? "#94a3b8" : "#64748b";
|
|
|
|
const [step, setStep] = useState(0);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [showRolePicker, setShowRolePicker] = useState(false);
|
|
|
|
const [firstName, setFirstName] = useState("");
|
|
const [lastName, setLastName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [phone, setPhone] = useState("");
|
|
const [role, setRole] = useState("VIEWER");
|
|
const [password, setPassword] = useState("");
|
|
|
|
const handleNext = () => {
|
|
if (step === 0 && (!firstName.trim() || !lastName.trim())) {
|
|
toast.error("Validation", "First and last name are required");
|
|
return;
|
|
}
|
|
if (step === 1 && (!email.trim() || !phone.trim())) {
|
|
toast.error("Validation", "Email and phone are required");
|
|
return;
|
|
}
|
|
if (step < STEPS.length - 1) setStep(step + 1);
|
|
};
|
|
|
|
const handleBack = () => {
|
|
if (step > 0) setStep(step - 1);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!password.trim()) {
|
|
toast.error("Validation", "Password is required");
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
try {
|
|
const formattedPhone = `+251${phone.trim()}`;
|
|
await api.team.create({
|
|
body: {
|
|
firstName: firstName.trim(),
|
|
lastName: lastName.trim(),
|
|
email: email.trim(),
|
|
phone: formattedPhone,
|
|
role,
|
|
password: password.trim(),
|
|
},
|
|
});
|
|
toast.success("Team Member Added", `${firstName} has been added to the team.`);
|
|
nav.back();
|
|
} catch (err: any) {
|
|
toast.error("Creation Failed", err.message || "Failed to add team member");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<FormFlow
|
|
steps={STEPS}
|
|
currentStep={step}
|
|
onNext={handleNext}
|
|
onBack={handleBack}
|
|
onComplete={handleSubmit}
|
|
loading={submitting}
|
|
completeLabel="Add Team Member"
|
|
>
|
|
{step === 0 && (
|
|
<View className="gap-5">
|
|
<View className="mb-2">
|
|
<Text className="font-sans-bold text-[18px] text-foreground">
|
|
Personal Info
|
|
</Text>
|
|
<Text className="mt-1 font-sans-medium text-[13px] text-muted-foreground">
|
|
Enter the team member's full name
|
|
</Text>
|
|
</View>
|
|
<Field
|
|
label="First Name"
|
|
value={firstName}
|
|
onChangeText={setFirstName}
|
|
placeholder="First name"
|
|
icon={<User size={16} color={iconColor} />}
|
|
/>
|
|
<Field
|
|
label="Last Name"
|
|
value={lastName}
|
|
onChangeText={setLastName}
|
|
placeholder="Last name"
|
|
icon={<User size={16} color={iconColor} />}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{step === 1 && (
|
|
<View className="gap-5">
|
|
<View className="mb-2">
|
|
<Text className="font-sans-bold text-[18px] text-foreground">
|
|
Contact Details
|
|
</Text>
|
|
<Text className="mt-1 font-sans-medium text-[13px] text-muted-foreground">
|
|
How to reach this team member
|
|
</Text>
|
|
</View>
|
|
<Field
|
|
label="Email Address"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
placeholder="email@company.com"
|
|
icon={<Mail size={16} color={iconColor} />}
|
|
/>
|
|
<View>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
Phone Number
|
|
</Text>
|
|
<View style={[S.input, { backgroundColor: useInputColors().bg, borderColor: useInputColors().border, flexDirection: "row", alignItems: "center" }]}>
|
|
<Phone size={16} color={iconColor} />
|
|
<Text style={{ marginLeft: 8, color: useInputColors().text, fontSize: 13, fontWeight: "600" }}>+251</Text>
|
|
<TextInput
|
|
style={{ flex: 1, marginLeft: 4, color: useInputColors().text, fontSize: 13, fontWeight: "500" }}
|
|
placeholder="912345678"
|
|
placeholderTextColor={useInputColors().placeholder}
|
|
value={phone}
|
|
onChangeText={setPhone}
|
|
keyboardType="phone-pad"
|
|
maxLength={9}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<View className="gap-5">
|
|
<View className="mb-2">
|
|
<Text className="font-sans-bold text-[18px] text-foreground">
|
|
System Access
|
|
</Text>
|
|
<Text className="mt-1 font-sans-medium text-[13px] text-muted-foreground">
|
|
Set role and initial password
|
|
</Text>
|
|
</View>
|
|
|
|
<View>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
Role
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowRolePicker(true)}
|
|
style={[S.input, { backgroundColor: useInputColors().bg, borderColor: useInputColors().border, flexDirection: "row", alignItems: "center" }]}
|
|
>
|
|
<ShieldCheck size={16} color={iconColor} />
|
|
<Text style={{ flex: 1, marginLeft: 8, color: useInputColors().text, fontSize: 13, fontWeight: "500" }}>
|
|
{role.replace("_", " ")}
|
|
</Text>
|
|
<ChevronDown size={16} color={iconColor} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<Field
|
|
label="Initial Password"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="••••••••"
|
|
icon={<Lock size={16} color={iconColor} />}
|
|
secureTextEntry
|
|
/>
|
|
</View>
|
|
)}
|
|
</FormFlow>
|
|
|
|
<PickerModal
|
|
visible={showRolePicker}
|
|
onClose={() => setShowRolePicker(false)}
|
|
title="Select Role"
|
|
>
|
|
{ROLES.map((r) => (
|
|
<SelectOption
|
|
key={r}
|
|
label={r.replace("_", " ")}
|
|
value={r}
|
|
selected={role === r}
|
|
onSelect={(v) => {
|
|
setRole(v);
|
|
setShowRolePicker(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|