diff --git a/cypress/e2e/customers/customers-create-edit.cy.ts b/cypress/e2e/customers/customers-create-edit.cy.ts
new file mode 100644
index 0000000..1784a55
--- /dev/null
+++ b/cypress/e2e/customers/customers-create-edit.cy.ts
@@ -0,0 +1,171 @@
+///
+
+describe("customers management - create and edit", () => {
+ beforeEach(() => {
+ cy.setAuthCookies();
+ });
+
+ it("validates required fields on create", () => {
+ cy.intercept("GET", "**/api/proxy/companies**", {
+ statusCode: 200,
+ body: { success: true, message: "ok", data: { companies: [] } },
+ }).as("getCompanies");
+
+ cy.visit("/customers/create");
+ cy.wait("@getCompanies");
+
+ cy.get('button[type="submit"]').click();
+ cy.contains("This field is required").should("be.visible");
+ });
+
+ it("validates invalid email on create", () => {
+ cy.intercept("GET", "**/api/proxy/companies**", {
+ statusCode: 200,
+ body: { success: true, message: "ok", data: { companies: [] } },
+ }).as("getCompanies");
+
+ cy.visit("/customers/create");
+ cy.wait("@getCompanies");
+
+ cy.get('input[name="username"]').type("qauser");
+ cy.get('input[name="email"]').type("not-an-email");
+ cy.get('select[name="role"]').select("admin");
+ cy.get('input[name="password"]').type("Secret123!");
+ cy.get('select[name="type"]').select("individual");
+ cy.get('button[type="submit"]').click();
+
+ cy.contains("Email address is invalid").should("be.visible");
+ });
+
+ it("creates customer and sends expected payload", () => {
+ cy.intercept("GET", "**/api/proxy/companies**", (req) => {
+ req.reply({
+ statusCode: 200,
+ body: {
+ success: true,
+ message: "ok",
+ data: {
+ companies: [
+ { id: "comp-1", name: "Acme" },
+ { id: "comp-2", name: "Globex" },
+ ],
+ },
+ },
+ });
+ }).as("getCompanies");
+
+ cy.intercept("POST", "**/api/proxy/customers", (req) => {
+ expect(req.body).to.include({
+ username: "newcustomer",
+ email: "newcustomer@example.com",
+ role: "admin",
+ type: "company",
+ });
+ req.reply({
+ statusCode: 201,
+ body: { success: true, message: "Customer created successfully", data: {} },
+ });
+ }).as("createCustomer");
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: {
+ success: true,
+ message: "ok",
+ data: { customers: [] },
+ meta: { offset: 0, limit: 10, total: 0, pages: 1, page: 1 },
+ },
+ }).as("getCustomersAfterCreate");
+
+ cy.visit("/customers/create");
+ cy.wait("@getCompanies");
+
+ cy.get('input[name="username"]').type("newcustomer");
+ cy.get('input[name="email"]').type("newcustomer@example.com");
+ cy.get('select[name="role"]').select("admin");
+ cy.get('input[name="password"]').type("Secret123!");
+ cy.get('select[name="type"]').select("company");
+ cy.get('button[type="submit"]').click();
+
+ cy.wait("@createCustomer");
+ cy.location("pathname").should("eq", "/customers");
+ cy.wait("@getCustomersAfterCreate");
+ });
+
+ it("loads edit page with customer details and updates customer", () => {
+ const customerId = "507f1f77bcf86cd799439155";
+ const customerDetails = {
+ id: customerId,
+ username: "existingcustomer",
+ email: "existing@example.com",
+ full_name: "Existing Customer",
+ phone: "1234567890",
+ role: "analyst",
+ type: "individual",
+ companies: [{ id: "comp-2", name: "Globex" }],
+ status: "active",
+ created_at: 1710001000,
+ updated_at: 1710001500,
+ };
+
+ cy.intercept("GET", "**/api/proxy/companies**", {
+ statusCode: 200,
+ body: {
+ success: true,
+ message: "ok",
+ data: {
+ companies: [
+ { id: "comp-1", name: "Acme" },
+ { id: "comp-2", name: "Globex" },
+ ],
+ },
+ },
+ }).as("getCompanies");
+
+ cy.intercept("GET", `**/api/proxy/customers/${customerId}`, {
+ statusCode: 200,
+ body: { success: true, message: "ok", data: customerDetails },
+ }).as("getCustomerDetails");
+
+ cy.intercept("PUT", `**/api/proxy/customers/${customerId}`, (req) => {
+ expect(req.body).to.include({
+ username: "existingcustomer",
+ email: "updated@example.com",
+ role: "admin",
+ type: "company",
+ });
+ req.reply({
+ statusCode: 200,
+ body: { success: true, message: "Customer updated successfully", data: {} },
+ });
+ }).as("updateCustomer");
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: {
+ success: true,
+ message: "ok",
+ data: { customers: [] },
+ meta: { offset: 0, limit: 10, total: 0, pages: 1, page: 1 },
+ },
+ }).as("getCustomersAfterUpdate");
+
+ cy.visit(`/customers/edit/${customerId}`);
+ cy.wait("@getCustomerDetails");
+ cy.wait("@getCompanies");
+
+ cy.get('input[name="username"]').should("have.value", "existingcustomer");
+ cy.get('input[name="email"]').should("have.value", "existing@example.com");
+ cy.get('select[name="role"]').should("have.value", "analyst");
+ cy.get('select[name="type"]').should("have.value", "individual");
+
+ cy.get('input[name="email"]').clear().type("updated@example.com");
+ cy.get('select[name="role"]').select("admin");
+ cy.get('select[name="type"]').select("company");
+ cy.get('button[type="submit"]').click();
+
+ cy.wait("@updateCustomer");
+ cy.location("pathname").should("eq", "/customers");
+ cy.wait("@getCustomersAfterUpdate");
+ });
+});
diff --git a/cypress/e2e/customers/customers-management.cy.ts b/cypress/e2e/customers/customers-management.cy.ts
new file mode 100644
index 0000000..38bc9fe
--- /dev/null
+++ b/cypress/e2e/customers/customers-management.cy.ts
@@ -0,0 +1,373 @@
+///
+
+const makeCustomer = (overrides: Partial> = {}, index = 1) => ({
+ id: `507f1f77bcf86cd7994390${String(index).padStart(2, "0")}`,
+ full_name: `Customer ${index}`,
+ email: `customer${index}@example.com`,
+ username: `customer${index}`,
+ phone: "1234567890",
+ type: "individual",
+ status: "active",
+ role: "admin",
+ companies: [],
+ created_at: 1710000000 + index,
+ updated_at: 1710000500 + index,
+ ...overrides,
+});
+
+const makeCompany = (overrides: Partial> = {}, index = 1) => ({
+ id: `company-${index}`,
+ name: `Company ${index}`,
+ email: `company${index}@example.com`,
+ phone: "1234567890",
+ website: "https://example.com",
+ type: "vendor",
+ description: "Test company",
+ founded_year: 2020,
+ employee_count: 25,
+ annual_revenue: 1000000,
+ currency: "USD",
+ industry: "Technology",
+ language: "en",
+ tax_id: `TAX-${index}`,
+ registration_number: `REG-${index}`,
+ timezone: "UTC",
+ address: {
+ street: "Main St",
+ city: "Tehran",
+ state: "Tehran",
+ postal_code: "12345",
+ country: "IR",
+ },
+ contact_person: {
+ full_name: "Jane Doe",
+ first_name: "Jane",
+ last_name: "Doe",
+ email: "jane.doe@example.com",
+ phone: "1234567890",
+ mobile: "1234567890",
+ position: "Manager",
+ is_primary: true,
+ },
+ tags: {
+ keywords: [],
+ categories: [],
+ specializations: [],
+ certifications: [],
+ cpv_codes: [],
+ },
+ ...overrides,
+});
+
+const customersResponse = ({
+ customers,
+ offset = 0,
+ limit = 10,
+ total = customers.length,
+ pages = Math.max(1, Math.ceil(total / limit)),
+}: {
+ customers: Record[];
+ offset?: number;
+ limit?: number;
+ total?: number;
+ pages?: number;
+}) => ({
+ success: true,
+ message: "ok",
+ data: { customers },
+ meta: { offset, limit, total, pages, page: Math.floor(offset / limit) + 1 },
+});
+
+describe("customers management - list, actions, filters, feedback", () => {
+ beforeEach(() => {
+ cy.setAuthCookies();
+ });
+
+ it("renders table columns and row data from API", () => {
+ const customers = [
+ makeCustomer(
+ {
+ full_name: "John Carter",
+ email: "john.carter@example.com",
+ companies: [{ id: "c1", name: "Acme" }],
+ },
+ 1,
+ ),
+ makeCustomer({ status: "inactive", role: "analyst" }, 2),
+ ];
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: customersResponse({ customers }),
+ }).as("getCustomers");
+
+ cy.visit("/customers");
+ cy.wait("@getCustomers");
+
+ [
+ "row",
+ "full name",
+ "email",
+ "created at",
+ "type",
+ "status",
+ "company",
+ "role",
+ "actions",
+ ].forEach((header) => cy.contains("th", new RegExp(`^${header}$`, "i")).should("be.visible"));
+
+ cy.contains("John Carter").should("be.visible");
+ cy.contains("john.carter@example.com").should("be.visible");
+ cy.contains("Acme").should("be.visible");
+ });
+
+ it("shows empty state when API returns no customers", () => {
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: customersResponse({ customers: [], total: 0 }),
+ }).as("getCustomersEmpty");
+
+ cy.visit("/customers");
+ cy.wait("@getCustomersEmpty");
+
+ cy.getByCy("empty-list-message").should("contain", "No Customers were found");
+ });
+
+ it("shows loading skeleton while list request is in flight", () => {
+ cy.intercept("GET", "**/api/proxy/customers**", (req) => {
+ req.reply({
+ delay: 1200,
+ statusCode: 200,
+ body: customersResponse({ customers: [makeCustomer()] }),
+ });
+ }).as("getCustomersSlow");
+
+ cy.visit("/customers");
+ cy.getByCy("table-skeleton").should("be.visible");
+ cy.wait("@getCustomersSlow");
+ cy.getByCy("table-skeleton").should("not.exist");
+ });
+
+ it("paginates with correct offset when moving to next page", () => {
+ const pageOne = Array.from({ length: 10 }).map((_, i) => makeCustomer({}, i + 1));
+ const pageTwo = [makeCustomer({ id: "507f1f77bcf86cd799439099", full_name: "Page 2 Customer" }, 99)];
+
+ cy.intercept("GET", "**/api/proxy/customers**", (req) => {
+ const offset = Number(req.query.offset ?? 0);
+ if (offset === 10) {
+ req.alias = "getCustomersPage2";
+ req.reply({
+ statusCode: 200,
+ body: customersResponse({
+ customers: pageTwo,
+ offset: 10,
+ limit: 10,
+ total: 11,
+ pages: 2,
+ }),
+ });
+ return;
+ }
+ req.alias = "getCustomersPage1";
+ req.reply({
+ statusCode: 200,
+ body: customersResponse({
+ customers: pageOne,
+ offset: 0,
+ limit: 10,
+ total: 11,
+ pages: 2,
+ }),
+ });
+ });
+
+ cy.visit("/customers");
+ cy.wait("@getCustomersPage1");
+ cy.contains("Page 2 Customer").should("not.exist");
+ cy.contains("›").click();
+ cy.wait("@getCustomersPage2")
+ .its("request.query.offset")
+ .should("eq", "10");
+ cy.contains("Page 2 Customer").should("be.visible");
+ });
+
+ it("deletes customer with confirm modal and sends correct request", () => {
+ const customer = makeCustomer({ id: "507f1f77bcf86cd799439066", full_name: "Delete Me" }, 66);
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: customersResponse({ customers: [customer] }),
+ }).as("getCustomers");
+
+ cy.intercept("DELETE", `**/api/proxy/customers/${customer.id}`, {
+ statusCode: 200,
+ body: { success: true, data: { message: "Customer deleted successfully" } },
+ }).as("deleteCustomer");
+
+ cy.visit("/customers");
+ cy.wait("@getCustomers");
+ cy.contains("Delete Me").should("be.visible");
+
+ cy.get('button[data-tooltip-id="delete"]').first().click();
+ cy.getByCy("confirmation-modal").should("be.visible");
+ cy.getByCy("confirmation-modal-cancel").click();
+ cy.getByCy("confirmation-modal").should("not.exist");
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: customersResponse({ customers: [], total: 0 }),
+ }).as("getCustomersAfterDelete");
+
+ cy.get('button[data-tooltip-id="delete"]').first().click();
+ cy.getByCy("confirmation-modal-confirm").click();
+ cy.wait("@deleteCustomer");
+ cy.wait("@getCustomersAfterDelete");
+ cy.getByCy("empty-list-message").should("contain", "No Customers were found");
+ });
+
+ it("assigns customer to company with correct payload", () => {
+ const customer = makeCustomer(
+ { id: "507f1f77bcf86cd799439033", companies: [{ id: "comp-2", name: "Global LLC" }] },
+ 33,
+ );
+ const companies = [
+ makeCompany({ id: "comp-1", name: "Acme Corp" }, 1),
+ makeCompany({ id: "comp-2", name: "Global LLC" }, 2),
+ ];
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: customersResponse({ customers: [customer] }),
+ }).as("getCustomers");
+
+ cy.intercept("GET", "**/api/proxy/companies**", {
+ statusCode: 200,
+ body: {
+ success: true,
+ message: "ok",
+ data: { companies },
+ meta: { offset: 0, limit: 10, total: companies.length },
+ },
+ }).as("getCompanies");
+
+ cy.intercept("POST", `**/api/proxy/customers/${customer.id}/companies`, (req) => {
+ expect(req.body).to.deep.equal({ company_ids: ["comp-2"] });
+ req.reply({
+ statusCode: 200,
+ body: { success: true, message: "Selected Companies were assigned", data: {} },
+ });
+ }).as("assignCompany");
+
+ cy.visit("/customers");
+ cy.wait("@getCustomers");
+ cy.get('button[data-tooltip-id="assign-company"]').first().click();
+ cy.wait("@getCompanies");
+ cy.getByCy("modal-confirm-button").click();
+ cy.wait("@assignCompany");
+ });
+
+ it("surfaces assign-company API failures (404)", () => {
+ const customer = makeCustomer(
+ { id: "507f1f77bcf86cd799439034", companies: [{ id: "comp-1", name: "Acme Corp" }] },
+ 34,
+ );
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: customersResponse({ customers: [customer] }),
+ }).as("getCustomers");
+
+ cy.intercept("GET", "**/api/proxy/companies**", {
+ statusCode: 200,
+ body: {
+ success: true,
+ message: "ok",
+ data: { companies: [makeCompany({ id: "comp-1", name: "Acme Corp" }, 1)] },
+ },
+ }).as("getCompanies");
+
+ cy.intercept("POST", `**/api/proxy/customers/${customer.id}/companies`, {
+ statusCode: 404,
+ body: { error: { message: "Route not found" } },
+ }).as("assignCompany404");
+
+ cy.visit("/customers");
+ cy.wait("@getCustomers");
+ cy.get('button[data-tooltip-id="assign-company"]').first().click();
+ cy.wait("@getCompanies");
+ cy.getByCy("modal-confirm-button").click();
+ cy.wait("@assignCompany404").its("response.statusCode").should("eq", 404);
+ cy.getByCy("modal-content").should("be.visible");
+ });
+
+ it("applies status/role/type filters and keeps close action side-effect free", () => {
+ const unfiltered = [makeCustomer({ full_name: "Unfiltered Customer" }, 11)];
+ const filtered = [makeCustomer({ full_name: "Filtered Customer", status: "inactive" }, 12)];
+
+ cy.intercept("GET", "**/api/proxy/customers**", (req) => {
+ const isFiltered =
+ req.query.status === "inactive" &&
+ req.query.role === "analyst" &&
+ req.query.type === "company";
+ req.alias = isFiltered ? "getCustomersFiltered" : "getCustomersDefault";
+ req.reply({
+ statusCode: 200,
+ body: customersResponse({
+ customers: isFiltered ? filtered : unfiltered,
+ total: 1,
+ }),
+ });
+ });
+
+ cy.visit("/customers");
+ cy.wait("@getCustomersDefault");
+
+ cy.contains("button", "Filter").click();
+ cy.get('select[name="status"]').select("inactive");
+ cy.get('select[name="role"]').select("analyst");
+ cy.get('select[name="type"]').select("company");
+ cy.contains("button", "Search").click();
+
+ cy.wait("@getCustomersFiltered");
+ cy.contains("Filtered Customer").should("be.visible");
+
+ cy.contains("button", "Filter").click();
+ cy.contains("button", "Close").click();
+ cy.get("@getCustomersFiltered.all").should("have.length", 1);
+ });
+
+ it("navigates to feedback page and loads customer feedback list", () => {
+ const customer = makeCustomer({ id: "507f1f77bcf86cd799439044", full_name: "Feedback Target" }, 44);
+
+ cy.intercept("GET", "**/api/proxy/customers**", {
+ statusCode: 200,
+ body: customersResponse({ customers: [customer] }),
+ }).as("getCustomers");
+
+ cy.intercept("GET", "**/api/proxy/feedback**", (req) => {
+ expect(req.query.customer_id).to.eq(customer.id);
+ req.reply({
+ statusCode: 200,
+ body: {
+ success: true,
+ message: "ok",
+ data: [
+ {
+ id: "fb-1",
+ feedback_type: "bug",
+ created_at: 1710001000,
+ updated_at: 1710001200,
+ },
+ ],
+ },
+ });
+ }).as("getCustomerFeedback");
+
+ cy.visit("/customers");
+ cy.wait("@getCustomers");
+ cy.get('button[data-tooltip-id="feedback"]').first().click();
+ cy.location("pathname").should("eq", `/customers/feedback/${customer.id}`);
+ cy.wait("@getCustomerFeedback");
+ cy.contains("feedback type").should("be.visible");
+ });
+});