diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java new file mode 100644 index 00000000..e860e30e --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java @@ -0,0 +1,74 @@ +package com.example.petstoremobile.adapters; + +import android.graphics.Color; +import android.view.*; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.dtos.EmployeeDTO; +import java.util.List; + +public class EmployeeAdapter extends RecyclerView.Adapter { + + private List list; + private OnEmployeeClickListener listener; + + public interface OnEmployeeClickListener { + void onEmployeeClick(int position); + } + + public EmployeeAdapter(List list, OnEmployeeClickListener listener) { + this.list = list; + this.listener = listener; + } + + public static class EmployeeViewHolder extends RecyclerView.ViewHolder { + TextView tvFullName, tvUsername, tvEmail, tvPhone, tvRole, tvStatus; + + public EmployeeViewHolder(@NonNull View v) { + super(v); + tvFullName = v.findViewById(R.id.tvEmployeeFullName); + tvUsername = v.findViewById(R.id.tvEmployeeUsername); + tvEmail = v.findViewById(R.id.tvEmployeeEmail); + tvPhone = v.findViewById(R.id.tvEmployeePhone); + tvRole = v.findViewById(R.id.tvEmployeeRole); + tvStatus = v.findViewById(R.id.tvEmployeeStatus); + } + } + + @NonNull + @Override + public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_employee, parent, false); + return new EmployeeViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) { + EmployeeDTO e = list.get(position); + + holder.tvFullName.setText(e.getFullName() != null ? e.getFullName() : ""); + holder.tvUsername.setText("@" + (e.getUsername() != null ? e.getUsername() : "")); + holder.tvEmail.setText(e.getEmail() != null ? e.getEmail() : ""); + holder.tvPhone.setText(e.getPhone() != null ? e.getPhone() : ""); + + // Role badge + String role = e.getRole() != null ? e.getRole() : ""; + holder.tvRole.setText(role); + holder.tvRole.setBackgroundColor( + "ADMIN".equals(role) ? Color.parseColor("#1a759f") : Color.parseColor("#577590")); + + // Status badge + boolean active = Boolean.TRUE.equals(e.getActive()); + holder.tvStatus.setText(active ? "Active" : "Inactive"); + holder.tvStatus.setBackgroundColor( + active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336")); + + holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position)); + } + + @Override + public int getItemCount() { return list.size(); } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java b/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java new file mode 100644 index 00000000..bbd873c3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java @@ -0,0 +1,32 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.PageResponse; +import retrofit2.Call; +import retrofit2.http.*; + +public interface EmployeeApi { + + @GET("api/v1/employees") + Call> getAllEmployees( + @Query("page") int page, + @Query("size") int size); + + @GET("api/v1/employees/{id}") + Call getEmployeeById(@Path("id") Long id); + + @POST("api/v1/employees") + Call createEmployee(@Body EmployeeDTO employee); + + @PUT("api/v1/employees/{id}") + Call updateEmployee(@Path("id") Long id, @Body EmployeeDTO employee); + + @DELETE("api/v1/employees/{id}") + Call deleteEmployee(@Path("id") Long id); +} + + + + + + diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java new file mode 100644 index 00000000..21577a25 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java @@ -0,0 +1,102 @@ +package com.example.petstoremobile.dtos; + +public class EmployeeDTO { + + private long EmployeeId; + private Long userId; + private String username; + private String firstName; + private String lastName; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + private String createAt; + private String updatedAt; + + + // Constructor for create and update the employee + + + public EmployeeDTO(String username, String password, String firstName, String lastName, + String email, String phone, String role, boolean active) { + this.username = username; + this.password = password; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.phone = phone; + this.role = role; + this.active = active; + } + // password field for request only + private String password; + + + public long getEmployeeId() { + + return EmployeeId; + } + + public Long getUserId() { + + return userId; + } + + public String getUsername() { + + return username; + } + + public String getFirstName() { + + return firstName; + } + + public String getLastName() { + + return lastName; + } + + public String getFullName() { + + return fullName; + } + + public String getEmail() { + + return email; + } + public String getPhone() { + + return phone; + } + + public String getRole() { + + return role; + } + + public Boolean getActive() { + + return active; + } + + public String getCreateAt() { + + return createAt; + } + + public String getUpdatedAt() { + + return updatedAt; + } + + public String getPassword() { + + return password; + } + + +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java new file mode 100644 index 00000000..9602580b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -0,0 +1,141 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; +import android.text.*; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.EmployeeAdapter; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.StaffDetailFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.*; +import retrofit2.*; + +public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { + + private List employeeList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private EmployeeAdapter adapter; + private SwipeRefreshLayout swipeRefresh; + private EditText etSearch; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_staff, container, false); + + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + loadStaff(); + + FloatingActionButton fab = view.findViewById(R.id.fabAddStaff); + fab.setOnClickListener(v -> openDetail(-1)); + + ImageButton hamburger = view.findViewById(R.id.btnHamburgerStaff); + hamburger.setOnClickListener(v -> { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.openDrawer(); + }); + + return view; + } + + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewStaff); + adapter = new EmployeeAdapter(filteredList, this); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); + } + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchStaff); + etSearch.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int a, int b, int c) {} + public void afterTextChanged(Editable s) {} + public void onTextChanged(CharSequence s, int a, int b, int c) { + filter(s.toString()); + } + }); + } + + private void setupSwipeRefresh(View view) { + swipeRefresh = view.findViewById(R.id.swipeRefreshStaff); + swipeRefresh.setOnRefreshListener(this::loadStaff); + } + + private void filter(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(employeeList); + } else { + String lower = query.toLowerCase(); + for (EmployeeDTO e : employeeList) { + if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) + || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) + || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) + || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { + filteredList.add(e); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void loadStaff() { + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + RetrofitClient.getEmployeeApi(requireContext()).getAllEmployees(0, 100) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (r.isSuccessful() && r.body() != null) { + employeeList.clear(); + employeeList.addAll(r.body().getContent()); + filter(etSearch != null ? etSearch.getText().toString() : ""); + } else { + Toast.makeText(getContext(), "Failed to load staff", + Toast.LENGTH_SHORT).show(); + } + } + public void onFailure(Call> c, Throwable t) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Log.e("StaffFragment", t.getMessage()); + } + }); + } + + private void openDetail(int position) { + StaffDetailFragment detail = new StaffDetailFragment(); + Bundle args = new Bundle(); + if (position != -1) { + EmployeeDTO e = filteredList.get(position); + args.putLong("employeeId", e.getEmployeeId()); + args.putString("username", e.getUsername() != null ? e.getUsername() : ""); + args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); + args.putString("lastName", e.getLastName() != null ? e.getLastName() : ""); + args.putString("email", e.getEmail() != null ? e.getEmail() : ""); + args.putString("phone", e.getPhone() != null ? e.getPhone() : ""); + args.putString("role", e.getRole() != null ? e.getRole() : "STAFF"); + args.putBoolean("active", Boolean.TRUE.equals(e.getActive())); + args.putBoolean("isEditing", true); + } + detail.setArguments(args); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.loadFragment(detail); + } + + @Override + public void onEmployeeClick(int position) { + openDetail(position); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java new file mode 100644 index 00000000..2f02e684 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -0,0 +1,200 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.EmployeeApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.fragments.ListFragment; +import retrofit2.*; + +public class StaffDetailFragment extends Fragment { + + private TextView tvMode, tvStaffId; + private EditText etUsername, etPassword, etFirstName, etLastName, etEmail, etPhone; + private Spinner spinnerRole, spinnerStatus; + private Button btnSave, btnDelete, btnBack; + + private long employeeId = -1; + private boolean isEditing = false; + + private final String[] ROLES = {"STAFF", "ADMIN"}; + private final String[] STATUSES = {"Active", "Inactive"}; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_staff_detail, container, false); + initViews(view); + setupSpinners(); + handleArguments(); + + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> save()); + btnDelete.setOnClickListener(v -> confirmDelete()); + return view; + } + + private void initViews(View v) { + tvMode = v.findViewById(R.id.tvStaffMode); + tvStaffId = v.findViewById(R.id.tvStaffId); + etUsername = v.findViewById(R.id.etStaffUsername); + etPassword = v.findViewById(R.id.etStaffPassword); + etFirstName = v.findViewById(R.id.etStaffFirstName); + etLastName = v.findViewById(R.id.etStaffLastName); + etEmail = v.findViewById(R.id.etStaffEmail); + etPhone = v.findViewById(R.id.etStaffPhone); + spinnerRole = v.findViewById(R.id.spinnerStaffRole); + spinnerStatus = v.findViewById(R.id.spinnerStaffStatus); + btnSave = v.findViewById(R.id.btnSaveStaff); + btnDelete = v.findViewById(R.id.btnDeleteStaff); + btnBack = v.findViewById(R.id.btnStaffBack); + } + + private void setupSpinners() { + spinnerRole.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, ROLES)); + spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, STATUSES)); + } + + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.getBoolean("isEditing", false)) { + isEditing = true; + employeeId = a.getLong("employeeId", -1); + + tvMode.setText("Edit Staff Account"); + tvStaffId.setText("ID: " + employeeId); + tvStaffId.setVisibility(View.VISIBLE); + etUsername.setText(a.getString("username", "")); + etFirstName.setText(a.getString("firstName", "")); + etLastName.setText(a.getString("lastName", "")); + etEmail.setText(a.getString("email", "")); // ← was showing fullName + etPhone.setText(a.getString("phone", "")); + btnDelete.setVisibility(View.VISIBLE); + + // Pre-fill role + String role = a.getString("role", "STAFF"); + for (int i = 0; i < ROLES.length; i++) { + if (ROLES[i].equals(role)) { + spinnerRole.setSelection(i); + break; + } + } + + // Pre-fill status + boolean active = a.getBoolean("active", true); + spinnerStatus.setSelection(active ? 0 : 1); + + } else { + isEditing = false; + employeeId = -1; + tvMode.setText("Add Staff Account"); + btnDelete.setVisibility(View.GONE); + tvStaffId.setVisibility(View.GONE); + } + } + + + private void save() { + String username = etUsername.getText() != null ? etUsername.getText().toString().trim() : ""; + String password = etPassword.getText() != null ? etPassword.getText().toString().trim() : ""; + String firstName = etFirstName.getText() != null ? etFirstName.getText().toString().trim() : ""; + String lastName = etLastName.getText() != null ? etLastName.getText().toString().trim() : ""; + String email = etEmail.getText() != null ? etEmail.getText().toString().trim() : ""; + String phone = etPhone.getText() != null ? etPhone.getText().toString().trim() : ""; + String role = ROLES[spinnerRole.getSelectedItemPosition()]; + boolean active = spinnerStatus.getSelectedItemPosition() == 0; + + // Validation + if (username.isEmpty()) { etUsername.setError("Required"); return; } + if (!isEditing && password.isEmpty()) { + etPassword.setError("Required for new account"); return; + } + if (!isEditing && password.length() < 6) { + etPassword.setError("At least 6 characters"); return; + } + if (firstName.isEmpty()) { etFirstName.setError("Required"); return; } + if (lastName.isEmpty()) { etLastName.setError("Required"); return; } + if (email.isEmpty()) { etEmail.setError("Required"); return; } + if (phone.isEmpty()) { etPhone.setError("Required"); return; } + + EmployeeDTO dto = new EmployeeDTO( + username, + password.isEmpty() ? null : password, + firstName, + lastName, + email, + phone, + role, + active + ); + + Log.d("STAFF_SAVE", "isEditing=" + isEditing + + " employeeId=" + employeeId + + " username=" + username); + + EmployeeApi api = RetrofitClient.getEmployeeApi(requireContext()); + if (isEditing && employeeId > 0) { + api.updateEmployee(employeeId, dto).enqueue(simpleCallback("Updated successfully")); + } else { + api.createEmployee(dto).enqueue(simpleCallback("Staff account created")); + } + } + + private Callback simpleCallback(String msg) { + return new Callback<>() { + public void onResponse(Call c, Response r) { + Log.d("STAFF_SAVE", "Response: " + r.code()); + if (r.isSuccessful()) { + Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + try { + String err = r.errorBody().string(); + Log.e("STAFF_SAVE", "Error: " + err); + Toast.makeText(getContext(), "Error " + r.code() + ": " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("STAFF_SAVE", "Failed to read error"); + } + } + } + public void onFailure(Call c, Throwable t) { + Log.e("STAFF_SAVE", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }; + } + + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Staff Account?") + .setMessage("This will permanently delete this staff account.") + .setPositiveButton("Yes", (d, w) -> + RetrofitClient.getEmployeeApi(requireContext()) + .deleteEmployee(employeeId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + navigateBack(); + } + public void onFailure(Call c, Throwable t) { + Toast.makeText(getContext(), "Delete failed", + Toast.LENGTH_SHORT).show(); + } + })) + .setNegativeButton("No", null).show(); + } + + private void navigateBack() { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.getChildFragmentManager().popBackStack(); + } +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_staff_detail.xml b/android/app/src/main/res/layout/fragment_staff_detail.xml new file mode 100644 index 00000000..1b927b15 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_staff_detail.xml @@ -0,0 +1,233 @@ + + + + + + + +